docs(workshops): generation cascade workshop — 4 rounds, D-194 through D-218

Four-round workshop (Gestalt, Tyre, Paula, Burnelli-Sheldon, Miri)
mapping the full generation pipeline from planetary heightmap to
walkable tile. 25 D-records produced. Ticket dependency chain for
Tier 0-4 implementation identified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 20:18:17 +02:00
co-authored by Claude Opus 4.6
parent 288e3ccd2e
commit 42ee1f0a0e
18 changed files with 7575 additions and 0 deletions
@@ -0,0 +1,601 @@
---
title: "Round 3 — Burnelli-Sheldon: Economics as Generator Input"
description: "Mapping systems.db economic fields to spatial generator parameters across all scales"
type: workshop
status: active
workshop: generation-cascade
agent: burnelli-sheldon
round: 3
created: 2026-04-30
---
# Round 3 — Burnelli-Sheldon: Economics as Generator Input
The economics layer is built. The question is what the spatial pipeline should read
from it. My job here is to be the bridge: not to redesign the economics model, but to
map its concrete, queryable fields to concrete, actionable generator parameters.
I'll work scale by scale, then address differentiation, corporate footprint, dynamic
settlements, and what the simulation needs back.
---
## Preliminary: What Economic Data Actually Exists
Before prescribing anything, I surveyed what is actually in systems.db. The picture
is richer than the schema alone suggests.
**Per inhabited body (bodies table, 275 inhabited rows):**
- `economic_role`: 10 distinct values — `manufacturing`, `agricultural`, `extraction`,
`transit`, `research`, `commercial`, `service_mixed`, `mining`, `frontier`, `energy`
- `settlement_pattern`: 15+ values — `urban_concentrated`, `dispersed`, `dispersed_rural`,
`domed`, `underground_concentrated`, `cave`, `village_network`, `town_network`, etc.
- `industrial_corridor`: `MVG`, `Gate_Corp`, `DSMC`, `Prometheus`, `Agricultural_Syndic`
(null for many bodies — absence is meaningful)
- `population` (range: ~800 to 8.5B), `planet_class`, `atmosphere`, `surface_gravity`
**Per system (system_economy, system_factions, system_culture):**
- `economic_tier` (15), `economic_base_primary/secondary`, `distribution_index` (stratified/moderate)
- `currency_zone`: TRACTUS_PRIMARY | MARK_PRIMARY | MIXED
- `gate_energy_connected`: boolean
- `governance_type`, `dominant_faction`, `primary/secondary_fault_line`
- `cultural_register`, `ambient_anxiety`, `active_situation`
**Corporations (275 rows, `corp_presence` cross-reference):**
- `scope`: reach-wide, sector, system, local (many nulls — needs cleanup)
- `behavioral_archetype`: Monopolist | Distributor | Producer | Specialist | Cooperative | Intermediary
- `supply_chain_role`, `shadow_economy_access`
- `headquarters_system`, `headquarters_body`
**Corp presence per location:** The `corp_presence` table records which corps operate at
each body/station with `primary_operation` (commodity). Locations range from 1 corp
(frontier outpost) to 13 corps (GJ380c/Lendel — the Groombridge Settlement House hub).
This distribution is the single most useful signal for district composition.
**Commodities (36 types) and production chains (21 chains):** `production_ubiquity`
(monopolistic → ubiquitous) and `location_bound` (currently all 0, but the field is
load-bearing for future extractive operations).
**Brand products:** `brand_category`, `scarcity_class`, `base_premium_multiplier`,
`brand_tier` (halo/volume). These are spatial differentiators — halo brands require
physical presence; volume brands are ubiquitous.
**What is NOT yet queryable:** `behavioral_archetype` is mostly null. `scope` field on
corporations is a mixed bag (some are text prose, not enum values). The generator will
need to gracefully handle nulls on both.
---
## 1. Economic Data by Scale
### Regional Scale (planet → city hinterlands)
Regional subdivision is essentially hinterland characterization — what lies between
cities on a body. Economic signals at this scale:
**Primary input:** `bodies.economic_role` → hinterland type
| economic_role | Hinterland character | What fills the space |
|---|---|---|
| agricultural | Farmland belts, processing nodes | Field tiles, irrigation channels, silos |
| extraction / mining | Resource zones, access corridors | Open pits, pipeline corridors, camp clusters |
| manufacturing | Industrial fringe, supply chains | Warehousing strips, rail freight yards |
| transit | Corridor infrastructure | Road/rail density, relay stations |
| research | Campus periphery, exclusion zones | Restricted zones, observatory sites |
| commercial | Suburban sprawl, trade routes | Mixed low-density blocks, markets |
| service_mixed | Residential spread | Generic residential, civic amenities |
**Secondary input:** `atlas_roads`, `atlas_railroads` — road and rail count/density on
the body determines whether hinterland is connected or isolated. A body with 5+ rail
lines has industrial corridors linking cities; one with no rail has regional isolation.
**Tertiary input:** `system_economy.economic_tier` — tier 12 bodies have sparse
hinterland infrastructure regardless of `economic_role`. A tier-1 extraction world
looks like isolated mining camps; a tier-4 extraction world (Witwatersrand, GJ3522c)
has the industrial capacity to fill the hinterland.
**Recommendation:** The regional hinterland generator needs two parameters per body:
1. `HinterlandType` — derived from `economic_role`
2. `InfrastructureDensity` (0.01.0) — derived from `(economic_tier + rail_count + road_count) / normalization`
### City Scale (city → district decomposition)
The D-C4 decomposition formula (district count from population) is correct. The
economic question is: once you have N districts, what types are they?
**The mapping I propose is three-column: economic_role × settlement_pattern × distribution_index:**
#### economic_role → DistrictType distribution
I'll express this as a probability weight vector across the 9 confirmed DistrictType
values from Round 1/2 notes (Residential, Commercial, Industrial, Administrative,
LogisticsHub, Entertainment, MixedUse, Transit, Specialized):
| economic_role | Res | Com | Ind | Adm | Log | Ent | Mix | Trn | Spe |
|---|---|---|---|---|---|---|---|---|---|
| manufacturing | 25 | 5 | 35 | 5 | 20 | 2 | 5 | 3 | 0 |
| agricultural | 30 | 15 | 5 | 10 | 25 | 3 | 10 | 2 | 0 |
| extraction | 20 | 5 | 30 | 5 | 30 | 0 | 5 | 5 | 0 |
| transit | 10 | 20 | 5 | 5 | 25 | 10 | 15 | 10 | 0 |
| research | 20 | 5 | 5 | 15 | 5 | 5 | 10 | 0 | 35 |
| commercial | 15 | 35 | 5 | 5 | 10 | 15 | 15 | 0 | 0 |
| service_mixed | 25 | 20 | 5 | 10 | 5 | 15 | 20 | 0 | 0 |
| mining | 20 | 5 | 25 | 0 | 35 | 0 | 10 | 5 | 0 |
| frontier | 35 | 10 | 10 | 5 | 20 | 5 | 15 | 0 | 0 |
| energy | 10 | 0 | 20 | 5 | 30 | 0 | 5 | 5 | 25 |
These are not final tuning targets — they are the prior that seed generation should use
before corp-presence and governance modifiers are applied (see §3 below).
#### settlement_pattern modifier
`settlement_pattern` is a strong secondary signal. It doesn't change DistrictType
distribution — it changes *how districts are arranged*:
- `urban_concentrated`: compact grid, high-density blocks, minimal green space
- `dispersed` / `dispersed_rural`: districts smaller and more separated, low-density
building tiles, road-separated rather than contiguous
- `domed`: maximum 1 district (hardcoded — entire settlement is one pressurized dome);
DistrictType is always Specialized
- `underground_concentrated` / `cave`: vertical stacking matters; single large district
with strong perimeter treatment (Checkpoint or Walled)
- `village_network` / `town_network`: each "city" entry in atlas represents a network
of small settlements rather than one city — decompose into many Residential + small
Commercial; no Industrial districts unless industrial_corridor is set
#### distribution_index → prosperity gradient
`system_economy.distribution_index` gives the city-level prosperity shape:
- `stratified`: prosperity_index spans full range. A stratified manufacturing capital
has elite executive districts (0.8+) adjacent to worker housing (0.2). This creates
visible class geography.
- `moderate`: prosperity_index clusters around 0.40.6. The city is more economically
homogeneous — no dramatic wealth contrasts.
Concrete formula: if `distribution_index = stratified`, assign prosperity_index as
`0.1 + (district_rank / N) * 0.8` where district_rank is sorted by DistrictType
prestige (Administrative > Specialized > Commercial > Entertainment > Residential >
MixedUse > Industrial > LogisticsHub > Transit). If `moderate`, all districts draw
from N(0.5, 0.1) clipped to [0.2, 0.8].
### District Composition
Once district types are assigned, the generator needs to fill each district. Economic
data drives two things here: **tile density** and **building character**.
**Tile density from economic_role + DistrictType:**
A Commercial district on a manufacturing world differs from the same DistrictType on a
transit hub. The former is a small company store; the latter is a major trading floor.
Use this rule: `base_density = district_type_density[DistrictType] × role_multiplier[economic_role]`
Role multipliers (examples): transit on Commercial = ×1.8; manufacturing on Commercial
= ×0.5; research on Specialized = ×1.5; agricultural on LogisticsHub = ×1.4.
**Commodity signal → building archetype:**
The `corp_presence.primary_operation` field tells us what commodity each corp at this
location primarily deals in. Cross-referencing with `commodities.bulk_class`:
| bulk_class | Physical form → building archetype |
|---|---|
| bulk | Open yards, conveyor structures, grain silos |
| liquid | Tank farms, pipe corridors |
| precision | Clean-room buildings, elevated access points |
| perishable | Cold storage blocks, loading docks with shade cover |
| non_physical | Office towers, transmission arrays, no loading dock |
| standard | Generic warehouse / distribution |
This mapping is queryable: `SELECT cp.primary_operation, c.bulk_class FROM corp_presence cp JOIN commodities c ON cp.primary_operation = c.commodity_id WHERE cp.location_id = ?`
### Infrastructure: Roads, Rail, Ports
**Road quality:** `economic_tier + settlement_pattern → road_quality`
| tier | urban_concentrated | dispersed | domed |
|---|---|---|---|
| 1 | narrow_unpaved | tracks | internal_only |
| 2 | narrow_paved | gravel_road | internal_only |
| 3 | medium_paved | narrow_paved | internal_only |
| 4 | boulevard | medium_paved | internal_only |
| 5 | boulevard | boulevard | N/A |
**Rail placement:** Query `atlas_railroads` — if a body has railroad entries, the
generator should anchor rail yards in LogisticsHub districts and place station tiles in
Transit districts. No authoring needed; the atlas already marks rail presence.
**Port/spaceport location:** Transit and LogisticsHub districts should be placed at the
city edge closest to the body's orbital station (if `stations.orbits_body_id` matches).
If no station exists, spaceport tiles go at the district with highest `logistics`
weight. Port capacity is a direct read from `stations.docking_class`.
---
## 2. Economic Differentiation: Concrete Examples
Theory is cheap. Let me walk through three real bodies in systems.db to show how
economic fields produce spatially distinct results.
### Case A: Tributarium (GJ251c/city_0) — Manufacturing Capital
```
body.economic_role = manufacturing
body.settlement_pattern = urban_concentrated
body.industrial_corridor = Gate_Corp
body.population = 5B
system.distribution_index = stratified (GJ 251 — inferred from tier 5)
system.economic_tier = (reached via GJ251 system_economy)
corp_presence at GJ251c = gate-corporation (HQ system), kalten-assembly, precision-works...
```
**Generator output:**
- District composition: ~35% Industrial, ~20% LogisticsHub, ~25% Residential, ~5% Administrative
- Prosperity: sharply stratified — Gate Corp executive district at 0.9, worker housing at 0.15
- Gate Corp `behavioral_archetype = Monopolist` → one dominant industrial district type; consistent building footprint
- `industrial_corridor = Gate_Corp` → gate component manufacturing tiles dominate Industrial districts
- `gate_components` commodity: `bulk_class = precision` → clean-room buildings, not open yards
- Road network: tier 5 → boulevards; but industrial zones have dedicated freight lanes
### Case B: Bluebank (GJ3522c/city_0) — Extraction Capital
```
body.economic_role = extraction
body.settlement_pattern = urban_concentrated
body.industrial_corridor = DSMC
body.population = 1.5B
system.distribution_index = stratified
corp_presence = dsmc (DSMC HQ system)
```
**Generator output:**
- District composition: ~30% Industrial, ~30% LogisticsHub, ~20% Residential, ~5% Administrative, ~5% Transit
- `metallic_ore` + `rare_minerals` → bulk_class = bulk → open pit adjacency, conveyor structures, slag heap tiles
- DSMC = reach-wide Monopolist → single employer city; company housing blocks (uniform, low prosperity)
- No Entertainment districts. Very low prosperity (worker housing at 0.150.25)
- LogisticsHub → ship loading yards (bulk cargo configuration), railway terminus
**Contrast with Tributarium:** Both are industrial. Tributarium has precision manufacturing
(implants, gate components) → clean-room aesthetic, corporate towers. Bluebank has
raw extraction (ore, rare minerals) → open yard aesthetic, functional brutalism.
The aesthetic difference is entirely derivable from `production_ubiquity` and `bulk_class`.
### Case C: Strata (GJ280Ad/city_0) — Service Mixed Capital
```
body.economic_role = service_mixed
body.settlement_pattern = urban_concentrated
body.industrial_corridor = NULL
body.population = 1.8B
system.distribution_index = stratified
corp_presence = rare-vein-survey (HQ), adams-ford-publishing (HQ)
```
**Generator output:**
- District composition: ~25% Residential, ~20% Commercial, ~10% Administrative, ~15% Entertainment, ~20% MixedUse
- No industrial corridor → no dominant building archetype; district types are heterogeneous
- Two reach-wide HQ corps → two landmark buildings (publisher HQ, survey company HQ)
- `non_physical` services (financial, legal, entertainment) → office towers, no loading docks
- `distribution_index = stratified` + `service_mixed` → gentrified commercial district (prosperity 0.75) adjacent to mid-tier residential (prosperity 0.45)
**Contrast with Bluebank:** Strata has negligible Industrial districts. Its LogisticsHub
equivalents handle information rather than cargo — server banks, archive towers.
The physical infrastructure (roads, buildings) looks wealthy but not industrial.
---
## 3. The Corporation Footprint
Corporations are the most underutilized signal in the current bridge-gap. The
`corp_presence` table is already populated and queryable. Here is how it maps to space.
### Corporate Presence Tier
Derive a `CorpPresenceTier` per city from the corp_presence query:
```sql
SELECT COUNT(DISTINCT cp.corp_id) as corp_count,
COUNT(DISTINCT CASE WHEN co.scope = 'reach-wide' THEN cp.corp_id END) as halo_count,
SUM(CASE WHEN co.headquarters_system = b.system_id THEN 1 ELSE 0 END) as hq_count
FROM corp_presence cp
JOIN corporations co ON cp.corp_id = co.corp_id
JOIN bodies b ON cp.location_id = b.body_id
WHERE cp.location_id = ?
```
| corp_count | halo_count | tier | District effect |
|---|---|---|---|
| 12 | 0 | Company Town | Single employer → uniform district character |
| 35 | 01 | Local Economy | Mixed district types, small commercial |
| 610 | 12 | Regional Hub | Commercial district expands, entertainment emerges |
| 11+ | 3+ | System Nexus | Full district variety, landmark buildings |
GJ380c (Lendel) has 13 corps including 2 reach-wide HQs → System Nexus.
GJ1111b has 5 corps, 0 HQs → Local Economy.
This difference is real and pre-populated.
### HQ Presence → Corporate District
When `corporations.headquarters_system = atlas_city.body.system_id` AND
`headquarters_body = atlas_city.body_id` (or close enough), that city gets a corporate
district. Rules:
- One corporate district per reach-wide HQ corp at this city
- The corporate district's DistrictType is Administrative (if Monopolist) or Commercial (if Distributor/Specialist)
- `base_premium_multiplier` from brand_products by this corp → scales the landmark building height (halo ×18 brands get the skyline)
### Behavioral Archetype → District Shape
Where `behavioral_archetype` is populated:
| archetype | District character |
|---|---|
| Monopolist | Large uniform blocks, single building type, minimal commercial variety |
| Distributor | Dense transit + logistics districts; loading infrastructure heavy |
| Producer | Industrial districts with process-flow layout (input → process → output) |
| Specialist | One Specialized district with restricted perimeter; premium prosperity |
| Cooperative | Residential more integrated with commercial; lower wealth disparity |
| Intermediary | Commercial + financial services; mixed-use preferred |
Where `behavioral_archetype = NULL` (most corps currently), fall back to
`supply_chain_role` if populated, or derive from `primary_operation` commodity tier.
### Shadow Economy
`corporations.shadow_economy_access = 1` → this location has a shadow market.
Physical manifestation: one MixedUse district at the city fringe with reduced
perimeter treatment (Open instead of Gated), elevated tile decay (prosperity 0.150.25),
and elevated building density (informal construction = wasted space is illegal).
`system_fiscal.collection_efficiency < 0.7` at the system level → amplify shadow zone
size by `(1 - collection_efficiency) × 2` as a district probability weight modifier.
---
## 4. Dynamic Settlements (Off-Atlas)
The atlas marks cities. But economic pressure creates settlements the atlas doesn't
know about. These are procedural — generated from economic conditions, not authored.
### Trigger Conditions
**Mining camps** (highest frequency):
- Condition: `bodies.economic_role IN ('extraction', 'mining')` AND body has
`corp_presence` entries with `primary_operation IN ('metallic_ore', 'rare_minerals',
'lattice_grade_material')`
- Generator action: place 1 satellite settlement per 2 qualifying corps, at randomized
distance from nearest atlas city (15%40% of map scale)
- Settlement type: single LogisticsHub district + minimal Residential; no Commercial
- Size: 1 district, population ~1,00050,000
**Trade route waypoints**:
- Condition: `atlas_roads.kind IN ('highway', 'commercial')` with point_count > 5
(long road connecting two distant cities)
- Generator action: place a waypoint settlement at the geometric midpoint of roads
longer than a threshold
- Settlement type: Transit district (fueling stop, overnight), small Commercial
- Size: 1 district, population ~5005,000
**Agricultural dispersed nodes**:
- Condition: `bodies.settlement_pattern IN ('dispersed', 'dispersed_rural')` AND
`bodies.economic_role = 'agricultural'`
- Generator action: scatter farm clusters at interval proportional to `1/economic_tier`
(lower tier = fewer, sparser farms)
- Each farm cluster: 1 Residential district + 1 LogisticsHub, no perimeter treatment
- Size: sub-district scale
**Shadow economy nodes**:
- Condition: `system_fiscal.collection_efficiency < 0.6` AND system has shadow-viable
corps in `corp_presence`
- Generator action: place 1 informal settlement per system, adjacent to a major city
but not on its district grid
- Settlement type: MixedUse fringe; elevated decay, low prosperity
### What the Generator Needs Per Dynamic Settlement
Unlike atlas cities, dynamic settlements do not have authored names or economic roles.
The generator should derive:
- `settlement_seed = child_seed(body_seed, trigger_type_hash + location_hash)` — FNV-1a per D-010
- `economic_role` = inferred from trigger condition (mining camp → extraction, etc.)
- `prosperity_index` = base 0.15 for camps; 0.250.4 for waypoints
- `district_count` = always 1
- Name = procedural suffix of nearest atlas city ("Bluebank Camp 3", "Kallast Waypoint")
---
## 5. What the Economic Simulation Needs Back from the Spatial World
This is the other side of the bridge. Once districts exist, the simulation's trade
cost model gets better data.
### Transport Distance
The D-C4 formula places districts at city-local coordinates `(col × 512, row × 512)`.
After generation, the simulation needs:
```
city_transport_cost_modifier(from_city_id, to_city_id) → f32
```
Derived from: `atlas_roads` + `atlas_railroads` point_count between two cities on the
same body. Cities connected by rail get a 0.4× cost multiplier (per D-178 gate edge
512%); road-only cities get 1.0×; no infrastructure gets 2.5×.
This does not require the spatial world to be generated — it's derivable from atlas data
alone. The generator does not need to feed this back; it's a pre-generation query.
### Port Capacity
`stations.docking_class` (major/standard/restricted/none) is already in systems.db.
After generation, the port district's physical size (tile count in Transit districts)
should be reported back as `effective_port_size: u32`. This feeds the trade flow
capacity constraint: a Restricted port can't handle bulk cargo regardless of
economic_role.
Concrete mapping:
- `docking_class = major` → Transit district tile budget = 256 tiles; throughput_coefficient = 1.0
- `docking_class = standard` → 128 tiles; throughput_coefficient = 0.5
- `docking_class = restricted` → 32 tiles; throughput_coefficient = 0.2
- `docking_class = none` → no Transit district; throughput_coefficient = 0.0
### Infrastructure Quality
`settlement_pattern` + `economic_tier` → road quality (derived above). The simulation
should read `road_quality: RoadQuality` per body as a trade cost multiplier:
| road_quality | intra-city cost multiplier |
|---|---|
| narrow_unpaved | 3.0 |
| gravel_road | 2.0 |
| narrow_paved | 1.5 |
| medium_paved | 1.0 |
| boulevard | 0.7 |
This is already derivable from `economic_tier` + `settlement_pattern` without running
the generator — another pre-generation query, not a feedback dependency.
### What Actually Requires Generator Feedback
Most simulation needs are pre-generation derivable. The only fields that genuinely
require post-generation reporting:
1. **Effective port tile count** — physical district layout determines actual cargo
throughput, not just declared docking class
2. **Corp district allocation** — which DistrictType a given corp actually occupies,
once the district assignment algorithm runs (needed for corp financial state updates)
3. **Shadow economy footprint** — whether a shadow zone was generated, and its
prosperity floor (informs `collection_efficiency` updates)
These are three narrow feedback fields, not a broad data exchange. The simulation
should not depend on generator re-runs for routine tick processing.
---
## Responses to Round 2 Open Questions
### OQ-R2-1: Should `prosperity_index` be in the minimum slice?
**Yes.** And here is the economic argument for inclusion rather than deferral:
`prosperity_index` is not a decorative field — it is a readability signal derived
entirely from data we already query: `distribution_index` + `economic_role` +
`economic_tier`. The computation is trivial (one formula, no authoring). The cost of
deferral is not "decorative refinement later" — it is "all districts look equally
prosperous until a retrofit pass." Retrofit changes Phase 2 tile generation. Include
it in Stage 1 classification as a derived field; it costs 0.5 dev-days.
Concrete derivation formula:
```
base_prosperity = economic_tier / 5.0
role_modifier: manufacturing=0.0, extraction=-0.1, research=+0.15, service_mixed=+0.1,
commercial=+0.05, agricultural=-0.05, transit=-0.05, frontier=-0.2
if distribution_index = stratified:
district_prosperity[i] = (base + role_modifier) + (district_prestige_rank(i) - 0.5) * 0.7
else: # moderate
district_prosperity[i] = clamp(N(base + role_modifier, 0.1), 0.2, 0.8)
```
### OQ-R2-2: Should `political_archetype` be a formal authored field?
**Derive it; do not author it separately.** From an economist's perspective, this is
the classic "add a variable or derive it from existing ones" tradeoff. The answer is
always: derive unless you have reason to believe the derivation fails.
Derivation rules:
- `corp_presence` where one `behavioral_archetype = Monopolist` corp accounts for >60%
of corps → `CompanyTown`
- `governance_type = assembly` AND `commission_presence` high → `AdminCapital`
- `economic_role = transit` AND `corp_count > 6``FreePort`
- `primary_fault_line` AND `secondary_fault_line` both populated and active →
`Contested`
- Else → `OrganicGrowth`
If a named city fails this derivation in a way that's clearly wrong (the lead decides
this), add a one-field override in `atlas_cities` schema. But the starting position
should be: trust the data.
### OQ-R2-5: Naming register lookup table — before Phase 2?
**After walkable.** This is a pure naming question that has zero effect on tile layout.
The register taxonomy is correct and useful; the lookup table determines what *kind* of
name a district gets, not what tiles it contains. It can be authored once a district
exists and is legible. Defer.
---
## D-Record Candidates
### D-C9 (proposed by Burnelli-Sheldon): Economic Fields → Generator Parameter Mapping
The spatial generator's primary input from systems.db is the following read-only query
set, executed once per city at generation time:
```sql
-- All queries execute against server/data/systems.db (read-only)
-- City economic context
SELECT b.economic_role, b.settlement_pattern, b.industrial_corridor,
b.population, b.planet_class, b.atmosphere
FROM bodies b
JOIN atlas_cities ac ON ac.body_id = b.body_id
WHERE ac.city_id = ?;
-- System-level modifiers
SELECT se.economic_tier, se.distribution_index,
sf.dominant_faction, sf.governance_type,
ss.currency_zone, ss.gate_energy_connected
FROM system_economy se
JOIN system_factions sf ON se.system_id = sf.system_id
JOIN star_systems ss ON se.system_id = ss.system_id
WHERE se.system_id = (SELECT system_id FROM bodies b
JOIN atlas_cities ac ON b.body_id = ac.body_id
WHERE ac.city_id = ?);
-- Corporate presence at this body
SELECT co.corp_id, co.behavioral_archetype, co.scope, co.shadow_economy_access,
cp.primary_operation, c.bulk_class
FROM corp_presence cp
JOIN corporations co ON cp.corp_id = co.corp_id
LEFT JOIN commodities c ON cp.primary_operation = c.commodity_id
WHERE cp.location_id = (SELECT body_id FROM atlas_cities WHERE city_id = ?);
```
This data is sufficient to derive:
- `DistrictType` distribution weights per city
- `prosperity_index` per district
- `CorpPresenceTier` and corporate district placement
- `political_archetype` (derived, not authored)
- `perimeter_treatment` per district
- Infrastructure quality parameters
- Dynamic settlement trigger conditions
### D-C10 (proposed by Burnelli-Sheldon): economic_role → DistrictType Weight Table
The mapping table in §1 above (manufacturing → 35% Industrial, etc.) should be a
confirmed D-record, not just a design note. It is the foundational prior that all
district composition derives from. Corp-presence modifiers apply on top of it.
The table should live in Rust as a const array, keyed by `EconomicRole` enum. It can
be tuned later, but a locked canonical default prevents every sprint from relitigating
the same question.
---
## Summary: The Minimum Read Set
If the generator can only read one thing from systems.db per city, it should read
`bodies.economic_role`. Every other field is a refinement multiplier.
The full minimum read set for a differentiated Phase 1 district skeleton:
1. `bodies.economic_role` — district type distribution
2. `bodies.settlement_pattern` — district layout and density mode
3. `system_economy.economic_tier` — infrastructure quality and wealth floor
4. `system_economy.distribution_index` — prosperity gradient shape
5. `corp_presence` count at this body — corporate district intensity
6. `corporations.headquarters_system` match — HQ landmark flag
Everything else (behavioral_archetype, shadow economy, bulk_class, brand premiums,
currency zone) is Phase 2 enrichment — meaningful, but not required for the minimum
walkable world.
The pipeline will produce economically differentiated worlds on those six fields alone.
Everything else makes them richer.
@@ -0,0 +1,505 @@
---
title: "Round 4 — Burnelli-Sheldon: Economics Integration for Planet-Down Cascade"
description: "Economics domain requirements for the planet-down brief: layer entry points, determinism split, dynamic settlement latency, and per-layer DB field mapping"
type: workshop
status: active
workshop: generation-cascade
agent: burnelli-sheldon
round: 4
created: 2026-04-30
---
# Round 4 — Burnelli-Sheldon: Economics Integration for Planet-Down Cascade
The lead has reframed the cascade as planet-down. This changes the framing, not the
economics. My job this round is to map my accepted contributions from Round 3 into the
new four-layer model and answer the specific questions the lead flagged.
---
## 1. Where Does Economic Data First Enter?
**Answer: Layer 1, weakly. Layer 2, in full.**
### Layer 1 — Empty World (weak influence)
The lead defines Layer 1 as "natural world, no civilization." That is exactly right.
Economics does not shape terrain. But there is one legitimate economics-adjacent
influence at this layer: **biome quality distribution**.
A planet was settled because it was worth settling. The body's `economic_role` in
systems.db is not a cause of its terrain — it is the *outcome* of someone deciding
this terrain was worth exploiting. That selection creates a plausibility constraint:
an `agricultural` world had to have arable soil; an `extraction` world had to have
accessible ore. If the Layer 1 generator produces a volcanic hellscape for a body
with `economic_role = agricultural`, the world is self-contradictory.
The fix is not to have economics drive terrain geometry. It is to use
`bodies.economic_role` + `bodies.planet_class` as a **biome probability prior** in
the sub-biome refinement step:
| economic_role | Sub-biome quality bias |
|---|---|
| agricultural | More temperate/arable cells, gentler slopes, fewer badlands |
| extraction / mining | More rugged terrain, higher mineral-rich designations |
| research | More terrain variety (observable geography = research value) |
| transit | More navigable terrain, natural passes and harbors |
| frontier | Unconstrained — hostile terrain is plausible |
| manufacturing | Near-neutral; manufacturing follows settlement, not terrain |
This is a prior, not a hard constraint. The biome generator can still produce a
difficult agricultural world (thin soil, reclamation history) — but the prior nudges
it toward plausibility. If this is too complicated for Layer 1, the alternative is
simply to trust that `planet_class` encodes this well enough already (`temperate`
planets are already the natural home of `agricultural` worlds) and leave Layer 1
fully physics-driven.
Either approach is defensible. I flag it because the contradiction risk is real and
cheap to address at Layer 1 rather than later.
**Economic fields at Layer 1:**
- `bodies.economic_role` — biome probability prior (optional)
- `bodies.planet_class`, `bodies.atmosphere`, `bodies.surface_gravity` — primary physical inputs
### Layer 2 — Population Overlay (primary entry point)
This is where economics enters in full force and where my Round 3 work connects
most directly. The lead's description — "economics onto geography, settlements anchor
where geography supports them, felled forests → farmland, road/rail networks, anchored
by wiki population counts" — is precisely what the economics data enables.
The `atlas_cities` table provides wiki-anchored city positions and populations. The
`bodies` table provides the economic character. The `corp_presence` table shows which
corporations cluster where. The `atlas_roads` and `atlas_railroads` tables provide
infrastructure. All of this is pre-computed and stored in systems.db.
Layer 2 is not city-internal planning — it is the hinterland: what fills the space
between cities. Economic role determines hinterland character:
| economic_role | Hinterland fill | Notes |
|---|---|---|
| agricultural | Cleared farmland, irrigation networks, processing nodes | "Felled forests → farmland" lives here |
| extraction | Access corridors, mining infrastructure, stockpile areas | Extends to resource deposit locations |
| manufacturing | Industrial fringe, rail freight yards, supply zones | Follows logistics corridors |
| transit | Dense road/rail, relay stations, fuel depots | The infrastructure IS the hinterland |
| research | Exclusion zones, observatory sites, quiet buffer | Sparse, low-density |
| service_mixed | Suburban residential spread, civic infrastructure | Generic low-density |
**Economic fields at Layer 2 (full list):**
From `bodies`: `economic_role`, `settlement_pattern`, `industrial_corridor`, `population`, `planet_class`
From `atlas_*` tables: city positions and populations, road/rail presence, POI kinds
From `system_economy`: `economic_tier` (infrastructure density), `economic_base_primary/secondary`
From `system_gates`: `gate_connections` (trade route intensity → transport infrastructure density)
From `corp_presence`: which corps operate here, `primary_operation` commodity
Dynamic settlement triggers (see §3 below) are also evaluated at Layer 2.
---
## 2. The Determinism Rule — Layout vs. Appearance
The lead's rule is economically clean and I agree with it completely:
> Economic sim's rolling state affects RENDERING (prosperity, repair) NOT LAYOUT
> (streets locked by seed).
This is how real cities work. Detroit's street grid did not change when its industrial
base collapsed. Buenos Aires' Palermo grid was not rezoned during financial crisis.
The buildings aged and emptied; the streets persisted.
Here is the full split as I see it:
### Seed-Locked (Layout) — Generated Once, Never Changed
These are determined by `(world_seed + CityGenerationContext)` at generation time:
- Street grid pattern and block geometry
- Building footprints and lot parcels
- District boundaries
- Perimeter treatment type (Open / Fenced / Walled / Gated / Checkpoint)
- Access point positions
- Settlement positions — including LATENT settlements (see §3)
- Corporate campus footprints (the campus exists even if the corp is failing)
- Port infrastructure physical extent
- Road quality tier (narrow_unpaved through boulevard)
### Economic-Sim-Dynamic (Appearance) — Updated at Runtime
These are driven by the economic simulation's rolling state:
- `prosperity_index` per district — the sim updates this; rendering reads it to
choose tile variants
- Building repair state — prosperity below threshold triggers decay tiles
(ChunkMutations record explicit player damage; general decay is a rendering parameter)
- Active vs. ghost status of latent settlements (see §3)
- Corporate signage and branding presence — corp `health_metric` from
`corp_financial_state` drives whether branded signage renders
- Stockpile visibility in logistics districts — active trade → visible cargo tiles;
depressed trade → empty yard tiles
- Lighting state — prosperity floor affects illumination (powered vs. dark windows)
### The Key Insight: Prosperity Is a Render Parameter, Not a Tile Mutation
The most important implication of this split is that `prosperity_index` should NOT
be implemented as tile-level ChunkMutations for routine decay. It is a single float
per district. The renderer reads it and applies a decay probability to tile selection.
When prosperity drops from 0.6 to 0.3:
- The streets don't change
- The building footprints don't change
- The tile SELECTION changes (cracked pavement variant instead of clean; boarded
window instead of lit; rust on the facade)
- This happens purely in the rendering path, with no world mutation
ChunkMutations remain reserved for player-caused or explicit-event damage — a specific
building explosion, a player-placed barricade. General economic decay is a render
parameter. This distinction matters for performance: prosperity changes don't generate
millions of tile mutations.
### The "Felled Forest → Farmland" Exception
The lead specifically mentioned this as a Layer 2 dynamic. It seems to contradict
the layout-lock rule. Reconciliation:
"Felled forests → farmland" is **regional-scale land use** (Layer 2), not
**city-scale tile layout** (Layer 4). The lock rule applies to city-internal geometry
(streets, buildings). Regional land use — what biome category fills the hinterland
between cities — CAN change as a Layer 2 update when economic conditions shift.
Concretely: if the agriculture corp expands, new farmland clears adjacent to existing
cities. This is a land-use tile change at biome-cell resolution (large tiles covering
hectares), not a building-by-building tile mutation. The distinction:
- Layer 2 regional land use: coarse-resolution, economics-driven, can update
- Layer 4 city street/building layout: fine-resolution, seed-locked, never changes
This is not a contradiction. It is a resolution boundary.
---
## 3. Dynamic Settlements in the Planet-Down Model
In Round 3, I proposed four types of economically-triggered dynamic settlements:
mining camps, trade route waypoints, agricultural dispersed nodes, and shadow economy
nodes. The planet-down model changes how these work.
### The Latency Principle
Under the determinism rule, settlement POSITIONS must be seed-locked at generation
time. But economic conditions change — a mine depletes, a trade route shifts. The
reconciliation is **latent settlements**:
> The generator places ALL economically plausible settlement positions at Layer 2.
> Whether each settlement is ACTIVE is determined by the economic sim's running state.
> An inactive settlement exists spatially — as ruins, as empty structures, as cleared
> ground — but it is dark, unmaintained, and depopulated.
This is economically accurate. Ghost towns exist. The buildings are there; no one
lives in them. The road to the closed mine still exists.
### How This Works Per Settlement Type
**Mining camps:**
- At Layer 2 generation: evaluate `corp_presence` against body. Every body with
`economic_role IN ('extraction', 'mining')` and 2+ corps operating `metallic_ore` or
`rare_minerals` gets N seed-derived camp positions (N = corp count / 2).
- Active status at runtime: `corp_financial_state.health_metric > 0.4` for the
operating corp → camp is active (populated, lit, maintained). Corp distressed or
dissolved → camp becomes ghost state (dark, decayed render variants).
- Why this is seed-locked: the geological deposit that caused the camp to be placed is
geological fact, not economic contingency. The deposit doesn't move when the corp
fails; the camp's position doesn't either.
**Trade route waypoints:**
- At Layer 2 generation: `atlas_roads` entries with `point_count > 5` (long roads) get
a seed-derived waypoint at the geometric midpoint.
- Active status: `gate_links` trade flow proxy (nearby system still connected and
populated) → active. If the terminal system is abandoned, the waypoint becomes a
ruin.
- Seed-locked because: the road was built; the physical clearing was made. Even
abandoned trade routes leave ruins.
**Agricultural dispersed nodes:**
- At Layer 2 generation: `settlement_pattern IN ('dispersed', 'dispersed_rural')` AND
`economic_role = 'agricultural'` → scatter farm cluster positions at interval derived
from `1/economic_tier`.
- Active status: `corp_presence` corp with `primary_operation = 'agricultural_produce'`
health_metric → active or fallow.
- Fallow farms look different from ghost towns: cleared land, overgrown structures,
but the clearing and track remain. These are the richest "economic decay visible in
the world" cases.
**Shadow economy nodes:**
- At Layer 2 generation: `system_fiscal.collection_efficiency < 0.6` on the parent
system → place one informal settlement adjacent to the largest atlas city, position
seed-derived.
- Active status: shadow viability is a structural condition, not a corp health metric.
If collection_efficiency recovers (enforcement crackdown), the settlement is still
there but its character changes (from busy informal market to quiet derelict cluster).
### What This Means for the Layer 2 Implementation
Layer 2 must query the full economics dataset and produce:
1. All atlas-city hinterland characterizations
2. All latent dynamic settlement positions (with type tag and activating corp/condition)
3. Regional land use grid (farmland, industrial fringe, wilderness, etc.)
None of this requires the economic sim to be running. It is all derived from the
snapshot in systems.db at world generation time. The sim then drives the active/ghost
flag as its rolling state changes.
---
## 4. Economic Data Per Cascade Layer — Full Mapping
### Layer 1 — Empty World
| Field | Source table | Usage |
|---|---|---|
| `economic_role` | bodies | Biome probability prior (optional) |
| `planet_class` | bodies | Primary terrain generation input |
| `atmosphere` | bodies | Surface physics input |
| `surface_gravity` | bodies | Terrain height ceiling |
Economics is a soft advisory at this layer. The primary inputs are physical.
### Layer 2 — Population Overlay
| Field | Source table | Usage |
|---|---|---|
| `city_id`, `center_row`, `center_col`, `population`, `kind` | atlas_cities | Settlement anchor positions and sizes |
| `economic_role` | bodies | Hinterland character |
| `settlement_pattern` | bodies | Population distribution mode |
| `industrial_corridor` | bodies | Corridor-specific infrastructure character |
| `economic_tier` | system_economy | Infrastructure density floor |
| `economic_base_primary/secondary` | system_economy | System-level hinterland bias |
| `gate_connections`, `gate_topology` | system_gates | Trade route intensity → transport density |
| `corp_id`, `location_id`, `primary_operation` | corp_presence | Corporate cluster positions |
| `behavioral_archetype` | corporations | HQ layout character |
| `road_*` entries | atlas_roads | Existing road network |
| `railroad_*` entries | atlas_railroads | Rail corridors |
| `collection_efficiency` | system_fiscal | Shadow node trigger condition |
| `health_metric` | corp_financial_state | Settlement active/ghost status at generation |
Dynamic settlement triggers are evaluated from this field set.
### Layer 3 — City-Level Planning (zoning)
This is where the 10×9 matrix applies. The City-Level Planning layer receives a
`CityGenerationContext` (the Phase 3 → Phase 5 handoff struct from Round 3 consensus)
and produces a **zoning map** — land-use assignments per parcel, constrained by
topography from Layer 1 and infrastructure from Layer 2.
| Field | Source | Usage |
|---|---|---|
| `political_archetype` | atlas_cities (Phase 3 derived) | Zoning mix character |
| `prosperity_index` | atlas_cities (Phase 3 derived) | Base wealth gradient for zoning |
| `founding_orientation` | atlas_cities (Phase 3 derived) | Grid orientation, oldest district direction |
| `road_entry_directions` | atlas_road_edges (Phase 3 derived) | Commercial spine anchor |
| `surrounding_biome` | atlas_regional_biomes (Phase 3 derived) | Topographic zoning constraints |
| `district_count` | CityGenerationContext | How many zones to carve |
| `economic_role` | bodies | 10×9 weight matrix row selector |
| `distribution_index` | system_economy | Prosperity gradient shape (stratified vs. moderate) |
| `corp_presence` count | corp_presence (query) | Corporate district intensity |
| `behavioral_archetype` | corporations | District layout shape (Monopolist → homogeneous) |
| `shadow_economy_access` | corporations | Informal district placement trigger |
| `collection_efficiency` | system_fiscal | Informal district size multiplier |
| `currency_zone` | star_systems | Regulatory character (Commission presence) |
| `governance_type`, `dominant_faction` | system_factions | Administrative district character |
**The 10×9 matrix in this context:**
The matrix from Round 3 still applies at Layer 3, but the framing shifts from "assign
district types" to "weight zoning categories." Topography from Layer 1 and
infrastructure from Layer 2 interact with the matrix weights:
- Industrial zones: weighted toward flat terrain near logistics access (road/rail from
Layer 2). If the only flat terrain is already occupied by the commercial spine,
Industrial zones shift to city fringe.
- Residential zones: weighted against flood plains (Layer 1 biome data), toward
terrain above the industrial elevation.
- LogisticsHub: always adjacent to road/rail entry points from Layer 2.
- Specialized (research): weighted toward elevated or secluded terrain.
The matrix provides the prior. Topography and infrastructure provide the constraints.
The product is a zoning map that is economically motivated AND geographically sensible.
**Prosperity gradient at Layer 3:**
`distribution_index = stratified` → the zoning map assigns a monotonic prosperity
gradient across district types. Highest prosperity zones go at the end of the road
spine (historic center); lowest at the industrial fringe. This is the "Cities
Skylines" version of the formula I proposed in Round 3.
`distribution_index = moderate` → zones cluster in the 0.40.6 prosperity band with
less variation. The zoning map is flatter economically.
### Layer 4 — Street-Level Rendering
Layer 4 receives the zoning map from Layer 3 and produces tile grids. Most of Layer 4
is seed-derived (street pattern, building footprints). Economics enters at two points:
**At generation time (seed-locked):**
| Field | Source | Usage |
|---|---|---|
| `bulk_class` | commodities (via corp_presence.primary_operation) | Building archetype (open yard vs. clean room vs. office) |
| `base_premium_multiplier` | brand_products | Landmark building height/prominence |
| `prosperity_index` | Per-district from Layer 3 | Initial tile variant selection |
| `scarcity_class` | brand_products | Frequency of branded signage tiles |
**At runtime (dynamic, no tile mutation):**
| Signal | Source | Render effect |
|---|---|---|
| `prosperity_index` (updated by sim) | Economic simulation | Tile variant selection (clean vs. decayed vs. ruined) |
| `corp_financial_state.health_metric` | Economic simulation | Corporate signage presence, maintenance appearance |
| Active/ghost settlement status | Economic simulation | Lighting, activity, population density rendering |
| `collection_efficiency` | Economic simulation | Shadow market visual density |
The runtime signals do not change tile positions. They change which tile variant renders
at each position. The implementation is: the rendering system reads `prosperity_index`
per district from the sim's current state and applies a decay curve to tile selection.
---
## 5. Data Quality Issues That Must Be Resolved Before Layer 3
I flagged two data quality issues in Round 3. They become blockers at Layer 3:
**Issue 1: `behavioral_archetype` is mostly NULL**
Of the 275 corporation rows I surveyed, `behavioral_archetype` is NULL on most records.
The fallback chain I proposed in Round 3 still applies:
`behavioral_archetype``supply_chain_role` → commodity tier derivation from
`primary_operation`.
But `supply_chain_role` is also sparse. The realistic near-term fallback is
commodity tier: a corp whose `primary_operation = 'metallic_ore'` is a Producer;
`'financial_services'` is an Intermediary; `'gate_components'` is a Specialist.
This derivation should be added to `import_economics.py` as a backfill step — it can
fill NULL `behavioral_archetype` rows from commodity tier with a low confidence flag.
This is not required for the minimum viable slice but it enriches Layer 3 zoning
quality significantly.
**Issue 2: `scope` field is mixed-type text**
Corporation `scope` values are a mix of enum-ish text ("reach-wide", "local") and
prose descriptions ("GJ 338B local; north corridor secondary"). The generator cannot
reliably parse prose scope descriptions.
For Layer 3 corporate district intensity, I use `corp_count` as the proxy (not scope).
But for landmark building placement, I need to identify reach-wide HQ corps. A cleanup
pass on `scope` to normalize it to an enum (reach_wide | sector | system | local) would
be the right fix. Until then, the generator should treat any `scope` value that doesn't
exactly match a known enum string as `system`-tier.
---
## 6. Open Questions This Round Raises
The planet-down model introduces new questions that Round 3 didn't address:
**OQ-R4-B1: Who owns the latent settlement active/ghost flag?**
I've proposed that settlement active/ghost status is driven by the economic sim at
runtime. But the sim needs to know which settlements are latent and what conditions
activate them. Does this flag live:
- (a) In the generated world state as a sim-readable component on each settlement entity?
- (b) In a systems.db Phase 3 output table (settlement positions + trigger conditions)?
- (c) Computed entirely at runtime from corp presence + health?
Option (b) is cleanest — Phase 3 stores latent settlement positions and trigger
conditions, and the sim reads them alongside `corp_financial_state`. This integrates
naturally with the `generate_regional.py` pipeline.
**OQ-R4-B2: Does the "felled forests → farmland" change generate ChunkMutations?**
I argued above that this is a Layer 2 regional-scale change (large cells, biome-level),
not a Layer 4 tile mutation. But practically: when an agricultural corp expands and new
farmland appears, does this register as ChunkMutations in the save file, or as a biome
grid update to systems.db (requiring `make regen-db`)?
I lean toward: regional land use changes are coarse enough that they update a
`regional_land_use` column in the biome grid at runtime (not a full regen-db), and
the Layer 4 renderer reads the coarse grid to determine what large-scale tile type
fills undeveloped hinterland cells. This is not ChunkMutations — it's a different
resolution.
This needs an answer before Phase 3 and Phase 5 teams start building their respective
land-use systems with incompatible resolution assumptions.
**OQ-R4-B3: Does `prosperity_index` apply at district-level or block-level?**
In Round 3 I defined it at district level (one float per district). Under the
planet-down model, with topography-constrained zoning, a single district might span
dramatically different terrain (the industrial hillside vs. the commercial valley
floor). Should prosperity be a per-district scalar, or a per-block modifier?
Per-district is simpler and consistent with the Phase 5 architecture (DistrictSkeleton
has one `prosperity_index`). Per-block would enable richer decay gradients (a district
where prosperity collapses from the periphery inward). I recommend per-district for
now with a note that the field can be moved to BlockSkeleton in a later pass if the
single-value model proves too coarse.
**OQ-R4-B4: How does `distribution_index = stratified` interact with topography?**
In a stratified system, the wealth gradient is steep — executive district at 0.9,
worker housing at 0.15. In the planet-down model, topography often already encodes
wealth geography (hilltops are historically desirable; industrial lowlands are not).
Do we:
- (a) Let the topographic constraint drive the gradient independently of distribution_index?
- (b) Use `distribution_index` to set gradient magnitude and topography to set direction?
- (c) Only apply distribution_index when topography is flat (no natural gradient)?
Option (b) is my recommendation: topography tells us WHICH end is high-prosperity;
distribution_index tells us HOW STEEP the gradient is. A stratified city on a
hill has elite zones at the top; a moderate city on a hill has mixed zoning across
all elevations.
---
## Summary: Requirements for the Brief
The brief should specify:
1. **Economics enters weakly at Layer 1** (biome probability prior from economic_role —
optional but recommended) and **fully at Layer 2** (hinterland character, settlement
placement, infrastructure density).
2. **The determinism split is:** Layer 4 street/building layout is seed-locked.
Economic sim updates `prosperity_index` per district (render parameter, not tile
mutation). Regional land use (Layer 2) CAN update at biome-cell resolution.
3. **Dynamic settlements are latent at generation:** All economically plausible
settlement positions are placed seed-derived at Layer 2. Active/ghost status is
driven by economic sim. Ghost settlements exist spatially — they just look
abandoned.
4. **The 10×9 matrix applies at Layer 3** (City-Level Planning), modified by
topographic constraints from Layer 1 and infrastructure access from Layer 2.
The matrix provides the zoning prior; terrain provides the spatial constraints.
5. **The minimum economics read set for Layer 3** is the same 6-field set from
Round 3, now delivered via the `CityGenerationContext` struct rather than direct
DB queries at generation time. This is already captured in D-C12.
6. **`prosperity_index` is a Layer 3 output** (derived from economics data, stored
in CityGenerationContext / atlas_cities) and a **Layer 4 runtime parameter** (the
sim updates it; the renderer reads it for tile variant selection).
7. **Data quality pre-work:** `import_economics.py` should add a `behavioral_archetype`
backfill pass from commodity tier before `generate_regional.py` runs. `scope` field
normalization is recommended but not blocking.
@@ -0,0 +1,287 @@
---
title: "Gestalt — Round 1: Systems Inventory"
description: "Pipeline inventory from the systems design perspective: layer states, system interactions, economics integration, and ticket dependency analysis"
type: workshop
status: active
workshop: generation-cascade
agent: gestalt
round: 1
created: 2026-04-30
---
# Generation Cascade — Round 1: Gestalt Inventory
**Focus:** System interactions across pipeline layers. Where does economics enter? Where do social sites, NPC population, and zone palettes enter? What's the minimum viable viewable world? What tickets formally block the right work?
---
## Pipeline Inventory — Layer by Layer
### Overview
| Layer | Name | Status | Input | Output | Key Code |
|-------|------|--------|-------|--------|----------|
| 1 | Galaxy / System definitions | **DONE** | Wiki markdown frontmatter | `systems.db` (star_systems, bodies) | `tooling/economy-db/import_economics.py` |
| 2 | Planetary terrain simulation | **DONE** | Body definition (planet_class, biome) | Elevation/temp/moisture/water grids (in-memory) | `tooling/planet-gen/planet_simulation.py` |
| 3 | Atlas markers (city placement) | **DONE** | Terrain grids + systems.db (population, settlement_pattern) | `markers.json` (cities, roads, rail, rivers, mountains, POIs) | `tooling/planet-gen/generate_atlas.py` |
| 4 | City naming | **DONE** | markers.json (empty name fields) + cultural_corridor | markers.json (named cities and features) | `tooling/planet-gen/gemma_naming.py` |
| 5 | Economics layer | **DONE** | Wiki TOMLs + brand templates + Rust binary | systems.db (corps, brands, supply chains, trade flows) | `tooling/economy-db/import_economics.py` |
| 6 | City → District decomposition | **MISSING** | City entry from markers.json (position, population, kind) | N district stubs with WorldTier/ComplexityTier/SettingType | Nothing exists |
| 7 | Phase 1: DistrictSkeleton generation | **STUB** | District classification from Layer 6 | Filled DistrictSkeleton (block grid, reservations, social sites, zone palette) | `server/src/simulation/generator.rs` — types only |
| 8 | Society profile resolution | **PARTIAL** | cultural_corridor + heritage roots from body/system | SocietyProfileRef → cultural behavioral inputs | #679 (done) but not wired to DistrictSkeleton |
| 9 | Phase 2: ChunkData tile generation | **MISSING** | DistrictSkeleton Phase 1 output | Filled 64×64 tile grid (WalkabilityMap + tile types) | Nothing connects generator to tile data |
| 10 | Heritage grammar application | **MISSING** | SocietyProfileRef + zone palette + chunk skeleton | Culturally modified tile layout | HeritageGrammarOverlay not implemented |
| 11 | NPC population (Phase 2) | **PARTIAL** | DistrictSkeleton.society_profile + social_sites | NpcManifest → NPC pool with triangle assignments | Triangle/template system is FULL (#107, #108 done); NpcManifest seeding from skeleton is missing |
| 12 | Chunk streaming to generator hookup | **STUB** | Player position → on-demand load request | Generator-filled ChunkData delivered to WalkabilityMap | `chunk_streaming.rs` — load/unload works but calls static v0.1 data, not generator |
---
## Layer Details
### Layer 15: What's Done
These layers are genuinely complete and pipeline-capable. Layer 3 (atlas generator) deserves particular attention — it does real terrain-aware computation: continent flood-fill, habitability scoring, river mouth detection, A* road routing, MST rail generation. The output is rich and the data is in systems.db. **The atlas is the last fully-working pipeline layer.**
The economics layer (Layer 5) is likewise complete: corporations, brands, supply chains, trade flows, fiscal systems are all in systems.db. There is no code bridging this data downstream to district/zone generation yet.
### Layer 6: City → District Decomposition — MISSING (Critical Gap)
This is the first entirely missing layer. The atlas produces city markers (position, population, kind: capital/city, economic_role). Converting a city into N districts with appropriate classifications requires:
- Population tier → district count formula (e.g. population ÷ 50,000 per district, min 1)
- city.kind + body.economic_role → DistrictType assignments
- body.planet_class + city position → SettingType (Urban/Agricultural/Maritime/etc.)
- system WorldTier → ComplexityTier ceiling
**No ticket formally covers this.** #899 (District skeleton generator — Phase 1) assumes this decomposition as a prerequisite input but does not scope it. This layer needs its own ticket.
### Layer 7: Phase 1 DistrictSkeleton — STUB, with a Critical Data Model Discrepancy
`server/src/simulation/generator.rs` has a complete DistrictSkeleton struct with all fields the workshop designed. Types compile. But:
1. **No generation logic exists.** Every field that requires reasoning (block grid ZoningType assignments, social site placement, reservation generation, zone palette selection) has stub type aliases:
- `ZoneDefinition = String`
- `SocietyProfileRef = String`
- `DistrictContext = String`
- `Era = String`
- `AccessPoint = String`
- `GuaranteeAuditResult = String`
2. **WorldTier enum does not match the workshop decision.** The code has:
```rust
enum WorldTier { Peripheral, Connected, Core }
```
The workshop (D-READY-2, workshop-outcomes.md) locked:
```rust
enum WorldTier { Epicenter, Regional, Backwater, Passage, Waypoint }
```
This is a blocker. #899 cannot be implemented correctly against the current enum. Must be fixed before any generation logic touches WorldTier.
### Layer 8: Society Profile Resolution — Partial
Ticket #679 (location-to-culture resolution system) is done. This means we can resolve cultural_corridor → heritage_root for any location. **But this output is not yet wired into DistrictSkeleton.society_profile.** The `SocietyProfileRef` field is a String stub — the culture system exists but isn't connected to the generation path.
This is a wiring gap, not a design gap. Relatively cheap to fix once Phase 1 generation logic exists.
### Layer 9: Phase 2 ChunkData Tile Generation — MISSING (Critical Gap)
This is the other critical missing layer. `chunk_streaming.rs` has a solid streaming architecture — load/unload around player position works correctly. But `walkability.load_chunk(coord)` creates empty chunks; it doesn't call any generator.
The v0.1 world tiles come from YAML files authored by hand (#577) — `server/data/locations/`. The generator never produces tiles. **There is no pipeline from DistrictSkeleton → actual walkable tile data.**
For Phase 2 to work:
- ChunkData needs a tile layer (beyond the walkability bool grid)
- A generator function must take BlockSkeleton + DistrictSkeleton and fill a 64×64 tile grid
- chunk_streaming.rs needs to call this generator on `ZLevelLoadState::Ungenerated` chunks
Existing pre-workshop tickets (#100 "Ungenerated → instantiation", #156 "Procedural district filler", #160 "Tier 3 procedural generator") pre-date the confirmed architecture. They need re-scoping against DistrictSkeleton/ChunkData.
### Layer 10: Heritage Grammar — MISSING
`HeritageGrammarOverlay` is designed (D-READY-9 from workshop) but no Rust type exists. This is Phase 2+ — it applies at chunk fill time. Not a blocker for the minimum viable slice, but it IS what makes tiles culturally distinguishable.
### Layer 11: NPC Population — Partial (More Done Than Expected)
The triangle system (`server/src/simulation/triangle.rs`) is **substantially implemented** — the workshop brief calls it "stubs" but the file is extensive: template schemas, triangle escalation tick system, NPC spawn patterns. Intra-template (#107) and cross-template (#108) triangle generation are both done.
What's missing is the Phase 2 pipeline connection: NpcManifest seeding from DistrictSkeleton.society_profile + social_sites. But the machinery to run NPCs once they're spawned exists. This means NPC population is a **wiring problem**, not a design or implementation gap.
### Layer 12: Chunk Streaming Hookup — Stub
`chunk_streaming.rs` has the right architecture for v0.3+ on-demand loading (commented explicitly in the file: "For v0.3+, the generator fills newly loaded chunks with terrain data"). The radius=8 default covers the entire v0.1 district, so nothing unloads. The system is ready for the generator hookup; it just needs the generator to exist.
---
## System Interactions Analysis
### Economics Layer → Generation Pipeline
The economics layer is rich but its connections downstream are **all implicit data, no code**.
| Economics Output | Where It Should Enter | Current State |
|-----------------|----------------------|---------------|
| `bodies.economic_role` (Agriculture/Refining/Transit/etc.) | Layer 6: district decomposition → DistrictType | Not wired |
| `bodies.cultural_corridor` | Layer 8: society profile → SocietyProfileRef | #679 done but not wired to skeleton |
| Corporation presence in system | Phase 1: DistrictSkeleton zone_palette modifiers (FactionModifier) | ZoneDefinition is a String stub |
| Supply chain density | Phase 1: DistrictType (Commercial/Industrial/Logistics) weighting | No connection |
| Brand presence | Phase 2 / Heritage grammar: signage, storefronts | HeritageGrammarOverlay not implemented |
| Trade flow volume | WorldTier assignment (high-traffic → higher tier) | No connection |
**Key design principle for Round 2:** The economics → generation bridge should be explicit in Layer 6. When we decompose a city into districts, the economic_role should directly drive DistrictType proportions:
- `economic_role=Agriculture` → mostly Agricultural setting + Industrial blocks for processing
- `economic_role=Refining` → Industrial + Specialized
- `economic_role=Transit` → Transit + Commercial + Logistics
This is a systems design decision that should produce D-records.
### Where Social Sites, NPC Population, and Zone Palettes Enter
| System | Entry Layer | Entry Mechanism | Current State |
|--------|-------------|----------------|---------------|
| Zone palettes | Phase 1 (Layer 7) | DistrictSkeleton.zone_palette (Vec<ZoneDefinition>) | ZoneDefinition = String stub |
| Social sites | Phase 1 (Layer 7) | DistrictSkeleton.social_sites (Vec<SocialSitePlacement>) | SocialSitePlacement struct exists; no generation code fills it |
| NPC pool | Phase 2 (Layer 11) | NpcManifest seeded from society_profile | NpcManifest not yet a type; society_profile is a String |
| Triangle assignments | Phase 1 (Layer 7) | DistrictSkeleton.social_sites[i].triangles | No generation code |
| Heritage grammar | Phase 2 (Layer 10) | HeritageGrammarOverlay applied at chunk fill time | Not implemented |
**Critical ordering constraint:** Zone palettes must be determined at Phase 1 before chunk tiles can be generated in Phase 2. Social sites must be placed before NPC positions can be assigned. The dependency chain inside Phase 1 is:
```
Classification (WorldTier, ComplexityTier, SettingType)
→ Block grid assignment (ZoningType per block)
→ Zone palette selection (economics + era + culture)
→ Social site placement (template + block assignment)
→ Triangle assignment (TrianglePurpose per site)
→ Guarantee audit (3-tier checks)
```
The Phase 2 chain continues:
```
DistrictSkeleton (immutable)
→ ChunkData tile fill (floor/wall/zone tile types)
→ Heritage grammar overlay (cultural modifier)
→ NpcManifest instantiation (society_profile → NPC pool)
→ Triangle → NPC assignments (existing triangle system)
```
### Minimum Viable "Viewable World"
The thinnest vertical slice from heightmap to rendered tiles:
**Inputs available now:** One city entry from markers.json (position, population, kind). Body's planet_class, economic_role, cultural_corridor from systems.db.
**Minimum code to write:**
1. **Layer 6 stub**: Fixed formula `n_districts = max(1, population / 50_000)`. Assign one SettingType from planet_class. Assign WorldTier from system lookup. Output: N (WorldTier, ComplexityTier, SettingType) tuples.
2. **Layer 7 minimal**: Generate DistrictSkeleton with real block grid. Simple deterministic ZoningType assignment from DistrictType (e.g. Commercial district → mostly Commercial blocks, some Mixed). No social sites. No reservations. No guarantee audit. Zone palettes as named constants only.
3. **Layer 9 minimal**: For each BlockSkeleton, fill 4 chunks (2×2) with simple tile layout: exterior walls + floor interior + one door per edge. No rooms, no objects, no heritage grammar. Pure structural.
4. **Wiring**: In chunk_streaming.rs, on `load_chunk()` when no YAML data exists, call the Phase 2 generator instead of returning empty. The existing tilemap renderer already handles ChunkData.
**What this produces:** A player standing in a procedurally-generated city block. Walls are walls. Floor is floor. No cultural character. No NPCs. No social sites. But it's walkable, streaming, and fully pipeline-driven — which is what the cascade deliverable requires.
**Estimated effort for minimum viable slice:** ~57 dev-days of focused server work (Layer 6: 0.5 days, Layer 7 minimal: 2 days, Layer 9 minimal: 2 days, wiring: 1 day, WorldTier enum fix: 0.5 days).
---
## Ticket Dependency Chain
### Current State of Referenced Tickets
| Ticket | Title | Status | Current Blockers | My Assessment |
|--------|-------|--------|-----------------|---------------|
| #897 | Generation pipeline cascade (this workshop) | backlog | — | Active; blocks #899 |
| #899 | District skeleton generator — Phase 1 | backlog | #897 | Correct scope, but misses Layer 6 and WorldTier fix |
| #615 | Tycoon small business owner starting state | backlog | — | Game content design, not generation-dependent |
| #616 | Broad economic verb vocabulary design | backlog | — | Pure content design, no generation dependency |
| #619 | Full character customisation | backlog | — | Phase 4 cosmetic UI, NOT generation-dependent |
| #681 | Apartment generator logic (server) | backlog | #615, #679(done) | Needs cascade blocker: Phase 1 skeleton + cultural context |
| #682 | Apartment visual rendering (client) | backlog | #681 | Correct — blocked by #681 |
| #694 | Character creation screen | backlog | #685(done) | Phase 4 UI, NOT generation-dependent |
### Character and Apartment Work: What Actually Needs Blocking
**#619 (Character customization) and #694 (Character creation screen):** These are cosmetic UI tickets for Phase 4 (Player Control Scheme). Character visual appearance is independent of world generation. **These should NOT be formally blocked behind generation pipeline work.** The player needs to look like something before the world needs to be generated. Do not add generation blockers here.
**#681 (Apartment generator):** This IS generation-dependent and needs additional blockers. The apartment:
- Lives in a building in a district → needs that district's zone context
- Uses cultural context → #679 done, but the **wiring to DistrictSkeleton** is not done
- Uses economic position → #615 (tycoon starting state) is a real dependency
Recommended: add formal blocker on #681 behind Phase 1 district skeleton working. The apartment generator cannot produce culturally-appropriate tile layouts without knowing the district's society_profile and zone_palette.
**#682 (Apartment visual rendering):** Correctly blocked by #681. No changes needed.
### New Tickets Needed
| Priority | Ticket | Scope | Blocked By |
|----------|--------|-------|-----------|
| **Blocker** | Fix WorldTier enum — Peripheral/Connected/Core → Epicenter/Regional/Backwater/Passage/Waypoint | Rename enum values in generator.rs to match workshop-locked decision | #897 |
| **Critical** | Layer 6: City → District decomposition | Convert atlas city markers to district count + classification stubs | None (can start after WorldTier fix) |
| **Critical** | Phase 2: Basic chunk tile generator | Fill 64×64 chunk from BlockSkeleton: walls + floor, no rooms | #899 |
| **High** | Connect chunk_streaming to Phase 2 generator | On `load_chunk()` for Ungenerated chunks, invoke Phase 2 generator | Phase 2 chunk tile generator |
| **High** | Wire society profile to DistrictSkeleton | Connect #679 location-to-culture output → DistrictSkeleton.society_profile | #899 |
| **Medium** | Re-scope #156 (Procedural district filler) | Align pre-workshop ticket to DistrictSkeleton architecture | Phase 2 chunk tile generator |
| **Medium** | Re-scope #100 (Ungenerated → instantiation) | Align to chunk_streaming + Phase 2 on-demand fill pattern | Phase 2 chunk tile generator |
### Formal Cascade Blocker Chain
```
#897 (this workshop — D-records)
└─▶ WorldTier enum fix
└─▶ Layer 6: City → District decomposition (new ticket)
└─▶ #899 Phase 1 DistrictSkeleton generation
└─▶ Phase 2: Basic chunk tile generator (new ticket)
└─▶ Connect chunk_streaming to generator (new ticket)
└─▶ Wire society profile to skeleton (new ticket)
└─▶ #681 Apartment generator (ADD formal block here)
└─▶ #682 Apartment visual rendering
#694 Character creation screen — NOT in this chain (Phase 4 UI work)
#619 Character customization — NOT in this chain (Phase 4 cosmetic work)
#616 Verb vocabulary — NOT in this chain (content design)
#615 Tycoon starting state — feeds #681, not generation pipeline
```
---
## Key Flags for Round 2 Discussion
### 1. WorldTier enum mismatch is a hard blocker
Before #899 can be correctly implemented, `WorldTier` in generator.rs must be renamed. This is a small code change but touches a core enum — it needs a D-record confirming the canonical values before the rename happens, to prevent the same drift occurring again.
### 2. The apartment generator dependency chain is currently incomplete
#681 is blocked only by #615 and #679. It should also be blocked by Phase 1 district skeleton generation. The apartment exists in a district — without district skeleton generation working, we can't produce the zone context that makes an apartment feel culturally appropriate vs. just a random room.
### 3. Layer 6 (City → District decomposition) has no ticket
This is the first link in the chain from atlas markers to walkable tiles, and it has no ticket and no code. Without it, #899 has no valid input. New ticket needed before #899 can be effectively scoped.
### 4. Phase 2 tile generation is the real gate on "viewable world"
Phase 1 skeletons are useful for planning and NPC/economic pre-computation. But the cascade deliverable is "walkable generated world" — that requires Phase 2 tile generation. The thinnest slice that produces rendered tiles requires Phase 2, even if that Phase 2 is initially just "floor tiles inside block boundaries, wall tiles at edges." The existing rendering pipeline will handle it.
### 5. The economics layer is rich and available — use it at Layer 6
We have `economic_role`, `cultural_corridor`, trade flow data, corporation presence — all in systems.db. Layer 6 (city → district decomposition) is the natural integration point where this data should shape district character. The design principle: **economics drives district type; culture drives zone palette; both come from systems.db data that already exists.**
---
## Summary Table
| Gap | Severity | Effort | Notes |
|-----|----------|--------|-------|
| WorldTier enum mismatch | Blocker | 0.5 days | Rename enum values, update all references |
| Layer 6: City → District decomposition | Critical | 0.51 day | New ticket, no design gaps just missing code |
| Phase 1 generation logic | Critical | ~23 days | #899, blocked by this workshop's D-records |
| Phase 2 chunk tile generator | Critical | ~2 days | New ticket, blocked by #899 |
| Chunk streaming → generator hookup | High | ~1 day | New ticket |
| Society profile → skeleton wiring | High | 0.5 days | Design done (#679), wiring only |
| Heritage grammar overlay (Rust type + logic) | Medium | ~34 days | Phase 2+, not on critical path for minimum slice |
| NpcManifest seeding from skeleton | Medium | ~1 day | Machinery exists, connection missing |
| Guarantee audit implementation | Low | ~2 days | Can defer until Phase 1 generation is stable |
@@ -0,0 +1,323 @@
---
title: "Gestalt — Round 2: Spatial Pipeline Proposal"
description: "Ordered implementation plan, thinnest vertical slice, stored-vs-derived transition, D-record candidates, and ticket dependency graph for the spatial generation pipeline"
type: workshop
status: active
workshop: generation-cascade
agent: gestalt
round: 2
created: 2026-04-30
---
# Generation Cascade — Round 2: Gestalt Proposal
**Scope note:** This proposal covers the spatial pipeline ONLY — from atlas city markers to walkable rendered tiles. Social sites, NPC population, heritage grammar, society profiles, and cultural overlays are out of scope. The one textual concern (place naming) is Paula's domain and does not block any spatial work.
---
## 1. Ordered Implementation Plan
Five sequential steps. Each one unlocks the next. Nothing can be parallelized safely until step 3 lands.
### Step 1 — WorldTier Enum Fix
**Effort: 0.5 days**
Rename enum values in `server/src/simulation/generator.rs`:
```rust
// Before:
enum WorldTier { Peripheral, Connected, Core }
// After (workshop-locked, L-3):
enum WorldTier { Epicenter, Regional, Backwater, Passage, Waypoint }
```
Update every reference in the codebase. This is a rename, not a redesign. Must land first — it is the type foundation for every subsequent step.
### Step 2 — City → District Decomposition (Layer 6)
**Effort: 1.5 days (two sub-tasks)**
**Sub-task 2a (0.5d): Single-district generator input**
For the minimum viable slice, take `markers.json city[0]` (the capital city) and produce one `DistrictInput` — the struct that feeds Phase 1. Full multi-district city layout (N districts per city, coordinate translation) is deferred.
```rust
struct DistrictInput {
district_id: DistrictId,
seed: u64, // derived: world_seed XOR hash(city_id) XOR district_index
world_tier: WorldTier, // derived: system WorldTier from systems.db
complexity: ComplexityTier, // derived: from world_tier ceiling + city.population
setting: SettingType, // derived: from body.planet_class + city.kind
district_type: DistrictType, // derived: from body.economic_role + city.kind
layout_mode: DistrictLayoutMode, // derived: from settlement_pattern
}
```
All fields are derived from inputs already available in systems.db + markers.json. No new stored data.
**Sub-task 2b (1d): Full city decomposition + coordinate translation**
After the minimum slice is working, extend to N districts per city:
- Population formula: `n_districts = max(1, city.population / 50_000)` (capped at city atlas pixel footprint)
- Atlas pixel → sim-tile coordinate mapping (Tyre's Gap B — see Section 4 below for the proposed formula)
- DistrictType assignment from economic_role for each district slot
This is the production-quality layer. The minimum slice does not need it.
### Step 3 — Phase 1 DistrictSkeleton Spatial Generation
**Effort: 2 days (Stages 12 only)**
Implement the two spatial stages that Phase 2 actually needs. Defer Stages 35 (reservations, social sites, guarantee audit) — they are enhancements to an already-walkable world, not prerequisites.
**Stage 1 — Classification (0.5d)**
From `DistrictInput`, write real values to:
- `world_tier`, `complexity`, `setting`, `layout_mode` (all trivial — copy from input)
- `district_type` (from input)
- `z_levels` (from SettingType: Urban=3, Agricultural=1, Station=4, etc.)
**Stage 2 — Block grid (1.5d)**
This is the non-trivial step. Assign `ZoningType` to each of the 16 blocks in the 4×4 grid:
- `DistrictType` determines the probability distribution across `ZoningType` values
- `DistrictLayoutMode` (Grid vs Organic) determines block placement and rotation
- `BlockSkeleton.density_pct` derived from district type and block position (center blocks denser)
- Seed-driven variation within the distribution — same seed always produces same grid
Example mapping:
| DistrictType | Zoning distribution |
|-------------|---------------------|
| Commercial | 50% Commercial, 25% Mixed, 15% Residential, 10% Transit |
| Industrial | 50% Industrial, 25% Restricted, 15% Mixed, 10% Transit |
| Residential | 60% Residential, 25% Mixed, 15% Recreational |
| LogisticsHub | 40% Transit, 30% Industrial, 20% Commercial, 10% Mixed |
Fields deferred (left as stubs for now): `reservations`, `corridors`, `social_sites`, `boundaries`, `guarantee_audit`, `zone_palette`, `society_profile`, `access_points`.
### Step 4 — Phase 2: Basic Chunk Tile Generator
**Effort: 1.5 days**
For each `BlockSkeleton`, fill 4 chunks (2×2 per block, so 64×64 tiles per chunk):
**Minimum tile vocabulary needed:**
```rust
// These are the ONLY tile types needed for a walkable generated slice
const TILE_FLOOR: TileId = "floor";
const TILE_WALL: TileId = "wall";
const TILE_DOOR: TileId = "door";
```
**Fill algorithm:**
1. Outer 2 tiles of each chunk = wall perimeter
2. Interior 60×60 = floor
3. Each block edge shared with an adjacent block gets one door placement (seeded position within center third of edge)
4. `GeneratorChunkData` (currently `Vec<bool>`) gets extended to carry `TileId` per cell
This produces structurally boring but fully-walkable districts. Every block is a room. Every room has doors connecting to neighbors. The renderer can already handle this — it reads `TileId` data.
**Note:** ZoningType per block should tint the floor tile identifier so zones are visually distinguishable at this stage (e.g. `"floor_commercial"`, `"floor_industrial"`) — one line change per block, but makes the output informative.
### Step 5 — Chunk Streaming → Generator Hookup
**Effort: 1 day**
Modify `WalkabilityMap::load_chunk()` to call the Phase 2 generator when a chunk coordinate has no YAML file. Requires:
- A `DistrictMap` resource that maps `ChunkCoord``DistrictId` + `BlockPosition` (Step 2b contributes to this; Step 2a version maps everything to the one test district)
- Phase 2 generator callable from chunk loading path (Step 4)
- Cache generated chunks in memory (don't regenerate on every access)
---
## 2. Thinnest Vertical Slice — Converged Definition
**Converging Gestalt (57d) and Tyre (9d) estimates:**
The discrepancy came from two sources: (1) Tyre's 2d estimate for city decomp vs my 0.5d, and (2) Tyre's Phase 1 estimate included workshop-outcomes.md effort figures for all 5 stages while I deferred stages 35.
With the stripped scope (no social sites, no heritage grammar, no society profiles), and with the "one district at world origin" definition of minimum viable, the converged estimate is:
| Step | Task | Effort |
|------|------|--------|
| 1 | WorldTier enum fix | 0.5d |
| 2a | Single-district input stub | 0.5d |
| 3 | Phase 1 stages 12 (classification + block grid) | 2d |
| 4 | Phase 2 minimal (wall + floor + door per block) | 1.5d |
| 5 | Chunk streaming hookup | 1d |
| — | **Total minimum** | **5.5 days** |
**What this produces:** A player can spawn in a generated city district, walk through structurally distinct (but visually basic) rooms, cross door thresholds between blocks, and see zone-tinted floor tiles. The streaming system loads/unloads chunks correctly. The pipeline is end-to-end driven by city data in systems.db + markers.json — no hand-authored YAML maps involved.
**What this explicitly does NOT include:**
- Step 2b (full N-district city layout and coordinate translation) — 1d, separate ticket
- Phase 1 stages 35 (reservations, social sites, guarantee audit) — deferred
- Vertical structures (z_levels > 1) — deferred to Phase 4 deliverable (2-floor test map)
- Tile variety (single TILE_FLOOR type per zone tint) — deferred to heritage grammar sprint
---
## 3. The Stored vs. Derived Transition
**The lead's question:** Where in the pipeline does the transition from explicitly-stored data to seed-based deterministic derivation happen?
**Answer: At the boundary between atlas markers and city → district decomposition (between Layer 5 and Layer 6).**
Everything at Layer 5 and above is stored because it required human judgment (hand-refinement #838), expensive computation (terrain simulation, A* routing), or external process output (Gemma naming). Everything at Layer 6 and below is derived because it has no human-refinement phase and the combinatorial space is too large to store.
### What Is Explicitly Stored
| Data | Where | Why stored |
|------|-------|-----------|
| Star system definitions | systems.db | Authored from wiki markdown |
| Body parameters (population, planet_class, economic_role) | systems.db | Authored; economically simulated |
| City positions (atlas_cities rows) | systems.db + markers.json | Generated once; hand-refinable (#838) |
| City populations | systems.db + markers.json | Authored from wiki population data |
| Road/rail path topology | markers.json | Expensive A* computation; spatial structure that cannot be cheaply re-derived |
| River/ocean/mountain features | markers.json | Authored or terrain-simulation output |
| City names | markers.json | Gemma output; expensive to regenerate |
### What Is Seed-Derived
| Data | Seed derivation | Why derived |
|------|----------------|------------|
| District count per city | `population / 50_000` — deterministic formula | No human judgment needed |
| District WorldTier/ComplexityTier | `system_tier` lookup + `population_tier` formula | Deterministic from stored inputs |
| DistrictSkeleton.seed | `world_seed XOR fnv1a(city_id) XOR district_index` | Deterministic identity |
| Block grid (4×4 ZoningType) | Seeded from `district.seed` | Too many blocks to author |
| Block placement (Organic mode offsets) | Seeded from `district.seed + block_position` | Same |
| Chunk tile layout | Seeded from `block.seed` | Too many chunks to author |
| Building door positions | Seeded from `chunk_coord + edge_id` | Same |
### The Save File Corollary
This stored-vs-derived split determines what the save file must contain:
- **Save file stores:** Delta layer only — `DamageOverlay` entries, player state, event history
- **Save file does NOT store:** Any base world data (chunk tiles, block grid) — these are rederived from the master seed at load time
- **Implication:** Changing the generation algorithm between saves would corrupt existing saves (rederived world no longer matches player's remembered state). This is the same problem as save migrations in Dwarf Fortress. This workshop should produce a D-record acknowledging the constraint.
### City Marker Schema (OQ-R1-C Resolution)
The city marker schema gap (Miri/Tyre Gap A3/Gap B) has a clear answer in this framework:
**Do not extend markers.json.** Instead, resolve `primary_function`, `planet_class`, `settlement_pattern` via systems.db cross-reference using `body_id`.
Rationale: city markers are the stored anchor. Extending the schema means every pipeline run must correctly populate new fields — another source of drift. The systems.db already has `economic_role`, `planet_class`, `settlement_pattern` on the `bodies` table. The city → district decomposition step queries these directly by `body_id`. This keeps markers.json as a pure spatial-plus-naming store and systems.db as the authoritative parameter store.
---
## 4. Atlas Coordinate Translation (Resolving OQ-R1-D)
Tyre's Gap B: no formula maps atlas pixel coordinates to sim-tile coordinates.
**Proposed formula:**
The atlas grid is 512×256 pixels. Each pixel represents a terrain cell. A city's `center: [row, col]` is in this pixel space.
For the minimum slice, we don't need this — we spawn one district at world origin. But for Step 2b (full city layout), we need it.
The relationship between atlas pixels and the world map is:
- One atlas pixel ≈ one degree of surface area at the sim abstraction level
- A district is 512×512 sim tiles = 256×256 visual tiles = 256m
- The world size at simulation scale is to be determined (this is an open spec question)
**Pragmatic proposal:** Assign one district per city for the minimum slice (Step 2a). Defer the coordinate translation to a spec decision in Round 3 or a separate architecture D-record. The question "how big is the walkable world relative to the atlas" is a scope decision, not a pipeline decision.
---
## 5. D-Record Candidates
Four D-records for the spatial pipeline. These can be filed after Round 3 convergence.
### D-CANDIDATE-1: Generation Pipeline Layer Definitions
**Canonical layer sequence for the spatial generation pipeline:**
| Layer | Name | Input | Output | Storage |
|-------|------|-------|--------|---------|
| 15 | Atlas layers (existing) | Wiki + authored content | systems.db, markers.json | Explicitly stored |
| 6 | City → District decomposition | City entry + systems.db body params | DistrictInput structs | Derived, not stored |
| 7 | Phase 1: DistrictSkeleton | DistrictInput | DistrictSkeleton (spatial stages) | Derived, not stored |
| 8 | Phase 2: ChunkData fill | BlockSkeleton + DistrictSkeleton | 64×64 tile grid | Derived, cached in session |
| 9 | Chunk streaming | Player position | On-demand Phase 2 invocation | Architecture layer |
The cliff between Layer 5 and Layer 6 is the stored-vs-derived transition (see D-CANDIDATE-2).
### D-CANDIDATE-2: Stored-vs-Derived Transition Point
Atlas marker data (city positions, populations, road topology) is explicitly stored because it has a human-refinement phase and is expensive to recompute. Everything at Layer 6 and below is seed-derived at runtime. Save files store only the delta layer (DamageOverlay + player state) — not base world data. Changing generation algorithms between a player's saves is a breaking change and must be treated as a migration.
**Implication for implementation:** The generation pipeline must be stable once a world seed is committed to. A player's save file encodes the world seed as a root; all base world data derives from it. Version the generation algorithm separately from the game version.
### D-CANDIDATE-3: Minimum Phase 1 Spatial Scope
Phase 1 DistrictSkeleton generation has two mandatory spatial stages and three optional stages. Only stages 12 are prerequisites for Phase 2 tile generation:
- **Stage 1 (mandatory):** Classification — WorldTier, ComplexityTier, SettingType, DistrictType, LayoutMode, z_levels
- **Stage 2 (mandatory):** Block grid — 4×4 BlockSkeleton array with ZoningType, density_pct, seed
- **Stage 3 (optional, adds value):** Reservations — MultiBlockReservation for parks, terminals, plazas
- **Stage 4 (optional, adds value):** Social sites — SocialSitePlacement (deferred to content pipeline work)
- **Stage 5 (optional, adds value):** Guarantee audit — spatial invariant checks (deferred to post-walkable sprint)
Phases 2 reads only Stage 1+2 output to generate walkable tiles. Stages 35 enrich an already-walkable world.
### D-CANDIDATE-4: City → District Decomposition Formula
- **District count:** `max(1, floor(city.population / 50_000))`, capped at world_tier district ceiling
- **WorldTier ceiling:** Epicenter → uncapped; Regional → 8; Backwater → 4; Passage → 2; Waypoint → 1
- **Seed chain:** `district.seed = world_seed XOR fnv1a(city_id) XOR district_index` (deterministic, no entropy)
- **DistrictType assignment:** Derived from body.economic_role (primary function) + city.kind (capital/city role)
- **Cross-reference source:** systems.db `bodies` table, not markers.json (avoids schema extension)
---
## 6. Ticket Dependency Graph (Spatial Pipeline Only)
```
NEW: Fix WorldTier enum (generator.rs rename)
├──▶ NEW: City → district decomposition — single district (Step 2a)
│ │
│ └──▶ #899 (rescoped): Phase 1 DistrictSkeleton — stages 1-2 only
│ │
│ ├──▶ NEW: Phase 2 — basic chunk tile generator (Step 4)
│ │ │
│ │ └──▶ NEW: Connect chunk_streaming to generator (Step 5)
│ │ │
│ │ └──▶ [MINIMUM VIABLE WALKABLE WORLD]
│ │
│ └──▶ NEW: Add missing DistrictSkeleton fields (Tyre Gap C — 0.5d)
│ (vertical_structure, breach_only_zones, derived_analysis as stubs)
└──▶ NEW: Full city decomposition + coordinate translation (Step 2b, follow-on)
(not required for minimum slice; unblocks multi-district cities)
```
### Existing Ticket Positions (Spatial Scope Only)
| Ticket | Title | Recommendation |
|--------|-------|---------------|
| #899 | District skeleton generator — Phase 1 | Rescope: stages 12 only, no social sites. Remove Stage 45 from scope. Add explicit dependency on WorldTier fix + new city decomp ticket. |
| #681 | Apartment generator (server) | Add formal block behind "Minimum Viable Walkable World" (chunk streaming hookup). Apartment zones require real district context. Current blockers (#615, #679) are insufficient. |
| #682 | Apartment visual rendering | Correctly blocked by #681. No changes. |
| #619 | Full character customisation | No generation dependency. Phase 4 work. Do not add generation blockers. |
| #694 | Character creation screen | No generation dependency. Phase 4 work. Do not add generation blockers. |
| #616 | Economic verb vocabulary | No generation dependency. Content design work, can proceed anytime. |
| #156 | Procedural district filler | Superseded by Phase 2 chunk tile generator. Close or mark wontfix; new ticket replaces it. |
| #100 | Ungenerated → instantiation | Superseded by chunk_streaming hookup ticket. Close or fold into new ticket. |
| #160 | Tier 3 procedural generator | Pre-workshop, unclear scope. Defer re-scoping to post-walkable sprint. |
### #615 Position (OQ-R1-F Resolution)
With the scope correction (no NPC work in this workshop), the #615 question is moot for this workshop. Tyre argued it should block behind generation pipeline; my position was content design can proceed independently. **With the lead's correction that #615 is NPC territory and out of scope, neither position is relevant to this workshop.** #615 should be addressed in the NPC pipeline planning sprint, not here.
---
## Summary
| Item | Answer |
|------|--------|
| Minimum viable slice effort | **5.5 days** (stripped scope) |
| Stored-vs-derived transition | **Between Layer 5 and Layer 6** — atlas markers are the last stored layer |
| City marker schema extension? | **No** — cross-reference systems.db bodies table by body_id |
| #619/#694 blocked by generation? | **No** — Phase 4 UI work, independent |
| #681 additional blocker needed? | **Yes** — behind "minimum viable walkable world" milestone |
| #899 scope change needed? | **Yes** — stages 12 only, strip Stage 45 |
| Pre-workshop tickets (#100, #156) | **Close/supersede** — new tickets replace them |
@@ -0,0 +1,564 @@
---
title: "Gestalt — Round 3: Phase 3 Spatial Pipeline Systems Design"
description: "Phase 3 layer map, economics bridge, LoD cascade, interesting-decisions analysis, and ticket dependency graph for the regional-to-city layer"
type: workshop
status: active
workshop: generation-cascade
agent: gestalt
round: 3
created: 2026-04-30
---
# Generation Cascade — Round 3 (Gestalt)
**Focus:** Phase 3 systems design — the layer between atlas output (city dots on heightmaps) and Phase 5 tile generation. What sits here, how the layers interact, which economics data drives which spatial decisions, where the LoD cuts are, and what creates interesting player choices at the regional scale.
**Given facts from Rounds 1-2 (not revisited):**
- WorldTier enum: Epicenter / Regional / Backwater / Passage / Waypoint
- Seed boundary: atlas markers are the last stored layer; everything below is seed-derived
- No markers.json extension — cross-reference systems.db bodies table
- Atlas coordinates are UI-only; walkable world uses city-local coords
- Phase 1 DistrictSkeleton minimum = Stages 1-2 only
- `prosperity_index` and `perimeter_treatment` are INCLUDED in DistrictSkeleton before Phase 2
- `political_archetype` is a formal field in atlas_cities, derived by default, authored override allowed
- Heritage grammar, NPC population, social sites — all out of scope
**Building on this round's peers:**
- Tyre (Round 3) designed the technical layers and algorithms. I treat those as settled where they overlap with my analysis and add the systems view on top.
- Paula (Round 3) designed the political signals and settlement hierarchy. I'm treating those proposals as strong candidates and mapping them to the generation input/output model.
---
## 1. The Phase 3 Layer Map
Let me be clear about what Phase 3 is mechanically: it is a **tooling phase that enriches systems.db**. Every Phase 3 output is committed offline by `generate_regional.py`. The Rust server reads it at startup; no Phase 3 computation runs at game time.
The layers form a dependency graph. Here is the full picture with inputs, outputs, and what consumes each output:
### Layer A — City Classification and Footprint
**Inputs:**
- `bodies` table: `settlement_pattern`, `economic_role`, `planet_class`, `world_tier`
- `atlas_cities` table: `population`, `kind`
- Economics tables: corporation presence by body, trade flow intensity
**Outputs (new columns on `atlas_cities`):**
- `footprint_radius_km: REAL` — physical extent; derived from population × density formula
- `political_archetype: TEXT` — CompanyTown | AdminCapital | FreePort | Contested | OrganicGrowth
- `prosperity_index: REAL` — 0.01.0 baseline prosperity for this city; district-level values derived from this
- `founding_orientation: TEXT` — PortFacing | RailHeadFacing | ResourceFacing | DefenseFacing | AdminFacing
- `political_archetype_override: TEXT` — NULL = always derived; non-NULL = wiki-authored value
**Consumed by:**
- Layer C (road graph — cities are nodes)
- Layer D (territorial status — city WorldTier is an input)
- Layer F (wilderness annotation — city footprints mark urban cells)
- Phase 5 district skeleton — via `CityGenerationContext` struct
- Phase 3 Atlas UI — displays footprint circles and archetype icons
**No dependencies.** Runs first.
---
### Layer B — Regional Biome Grid
**Inputs:**
- Planet simulation terrain output: biome grid (512×256), elevation, moisture, surface_water
**Outputs (new table `atlas_regional_biomes`):**
- 64×32 cells per body: `biome_class`, `mean_elevation`, `mean_moisture`, `terrain_roughness`, `is_coastal`, `water_fraction`
- Later augmented with `land_use` (Layer F) and `territorial_status` (Layer D)
**Consumed by:**
- Layer D (TerritorialStatus — biome class + proximity drives zone type)
- Layer F (wilderness annotation — land use derived from biome + urban proximity)
- Phase 5 district skeleton — biome context at city position informs surrounding district types
**Dependencies:** planet_simulation terrain data (already in memory during `generate_atlas.py`). Can run in the same pipeline pass. Technically parallel with Layer A.
---
### Layer C — Road Network Graph
**Inputs:**
- City positions (Layer A outputs or atlas_cities directly)
- `atlas_roads` and `atlas_railroads`: pixel-path road data
- Economics tables: corporation presence, trade flow intensity
**Outputs:**
- `atlas_road_nodes` table: city nodes + junction nodes with grid position
- `atlas_road_edges` table: segments with `kind`, `distance_px`, `maintenance_authority`
- `road_entry_directions: Vec<CardinalDirection>` per city — which edges roads enter from
**`MaintenanceAuthority` derivation rule (Paula's proposal, mapped to economics inputs):**
| Road endpoint types | Economics condition | → MaintenanceAuthority |
|--------------------|--------------------|----------------------|
| City ↔ City | admin road in atlas | Administrative |
| Corporate asset ↔ Corporate asset | same corporation in econ data | Corporate |
| City ↔ Sub-settlement | settlement prosperity-tracked | Communal |
| Any ↔ Any | high trade flow in econ data | Trade |
| Any ↔ Abandoned marker | no active tether | Abandoned |
**Consumed by:**
- Layer D (TerritorialStatus — road corridors affect zone boundaries)
- Layer F (wilderness — cells adjacent to roads get Corridor land use)
- Phase 5 district skeleton — `road_entry_directions` drives access point placement
- Phase 3 Atlas UI — renders road graph as connectivity overlay
**Dependencies:** Layer A (need city node list to snap road endpoints).
---
### Layer D — TerritorialStatus Zones
**Inputs:**
- Layer A outputs: city positions, WorldTier, political_archetype
- Layer B outputs: biome grid (terrain roughness affects zone boundaries)
- Layer C outputs: road graph (roads extend administrative reach)
- Economics: corporation asset locations, territory claims
**Outputs:**
- `territorial_status: TEXT` added to `atlas_regional_biomes` cells:
`CoreTerritory | FrontierTerritory | ExtractiveZone | ContestZone | WildernessBuffer | AbandonedZone`
**Derivation algorithm (systems design view):**
For each regional cell, score against multiple signals:
```
base_status = WildernessBuffer (default)
FOR each city C within range:
influence = C.world_tier_weight / distance_to_cell
if influence > FRONTIER_THRESHOLD:
base_status = FrontierTerritory
if influence > CORE_THRESHOLD:
base_status = CoreTerritory
IF any corporation has an active extraction node in this cell:
base_status = ExtractiveZone (overrides Frontier but not Core)
IF two cities with conflicting political archetypes both influence this cell:
base_status = ContestZone
IF cell was once CoreTerritory (historical) but city tether is Abandoned:
base_status = AbandonedZone
```
WorldTier weights: Epicenter → 5x; Regional → 3x; Backwater → 1x; Passage → 1.5x; Waypoint → 0.5x.
**Consumed by:**
- Layer F (wilderness annotation — TerritorialStatus determines settlement density)
- Phase 5 district skeleton — TerritorialStatus of surrounding region informs district `perimeter_treatment` baseline
- Phase 3 Atlas UI — renders zone overlays on regional map
- Naming system — ContestZone triggers DualNaming conditions
**Dependencies:** Layers A, B, C all needed.
---
### Layer E — Station Module Topology
**Inputs:**
- Bodies with `settlement_pattern = "orbital_only"`: `economic_role`, `population`
**Outputs:**
- `atlas_station_modules`: module list with type
- `atlas_station_connections`: module adjacency graph with passage type
**Design note:** Stations are handled as a separate code path. No biome data. No TerritorialStatus. The station's module topology is its "regional map" — it tells Phase 5 what zones to generate when a player enters the station. The `AdminCore` is always the hub; passage types (`Pressurized | Airlock | EVA | Service`) are the station equivalent of road quality.
**Consumed by:**
- Phase 5 district skeleton — station district types match module types
- Phase 3 Atlas UI — renders station topology diagram in wiki/implant app
**Dependencies:** Only station body data. Parallel with Layers BD.
---
### Layer F — Wilderness, Countryside, Settlement Hierarchy
**Inputs:**
- Layer A: city footprints (urban cells identified)
- Layer B: biome grid (biome_class per cell)
- Layer C: road graph (corridor cells identified)
- Layer D: TerritorialStatus per cell
- Economics: corporation presence, trade routes
**Outputs:**
- `land_use: TEXT` on `atlas_regional_biomes` cells:
`Urban | Agricultural | Wilderness | Industrial | Wasteland | Ocean | Impassable | Corridor | Ruins`
- New table `atlas_sub_settlements`: towns, outposts, waypoints placed along road corridors
- Each has: `settlement_type`, `population`, `political_tether` (city_id + relationship)
- `DualNaming` conditions flagged per cell/feature for contested naming regions
**`PoliticalTether` derivation (Paula's proposal, mapped to generator inputs):**
| Condition | → TetherRelationship |
|-----------|---------------------|
| Distance to city < threshold AND city is AdminCapital | Administered |
| Distance to city < threshold AND city is CompanyTown AND corp presence | Corporate |
| Two cities within similar distance with different archetypes | Contested |
| Distance to city > threshold, no corp presence | Independent |
| Nearest city is AbandonedZone | Abandoned |
**Consumed by:**
- Phase 5 district skeleton — wilderness districts use land_use to determine district type
- Phase 3 Atlas UI — renders countryside zones and sub-settlement markers
- Naming system — PoliticalTether drives naming register for sub-settlements
**Dependencies:** Layers A, B, C, D all needed. This is the last Phase 3 layer.
---
### Phase 3 Output to Phase 5: `CityGenerationContext`
Everything Phase 3 produces feeds into Phase 5 through this struct:
```rust
pub struct CityGenerationContext {
pub city_id: String,
pub political_archetype: PoliticalArchetype,
pub founding_orientation: FoundingOrientation,
pub prosperity_index: f32, // city baseline; district-level derived from this
pub surrounding_biome: BiomeClass, // from regional grid at city position
pub surrounding_territorial_status: TerritorialStatus, // from Layer D at city position
pub road_entry_directions: Vec<CardinalDirection>,
pub footprint_radius_km: f32,
pub district_count: u8, // from city decomp formula (Round 2)
}
```
This struct is populated by reading systems.db at game startup. Phase 5 uses it as the root input for all district generation.
---
## 2. The Economics Bridge
Economics data in systems.db is the hidden backbone of Phase 3. The pipeline already ran the economics layer (import_economics.py) before generate_atlas.py. Phase 3 consumes that output systematically.
Here is the full economics → spatial structure mapping:
### economic_role → political_archetype (direct derivation)
| economic_role | → political_archetype |
|--------------|----------------------|
| `corporate_extraction` | CompanyTown |
| `administrative_center` | AdminCapital |
| `transit_hub` | FreePort |
| `mixed_economy` | OrganicGrowth |
| `contested_territory` | Contested |
Override slot (`political_archetype_override`) allows wiki authors to correct counterintuitive results for named cities. The default derivation handles 90%+ of cases correctly.
### Corporation footprint → spatial structure
The economics tables track which corporations operate in which systems and what their role is. This drives:
1. **ExtractiveZone identification:** If a corporation has an active extraction operation in a body's regional zone, that zone gets `TerritorialStatus = ExtractiveZone` regardless of distance from administrative cities.
2. **Road MaintenanceAuthority:** Roads connecting corporation assets to their logistics chain are `Corporate` maintained. These roads have uniform quality within the corporate footprint — which means the quality boundary at the corporation's edge is visually sharp. The generator uses this: when a corporate road ends, the next segment drops to whatever the local standard is.
3. **Sub-settlement PoliticalTether:** Settlements near corporate assets that depend on that corporation's operations are `Corporate`-tethered. If the corporation ever leaves (detectable from economic data), those settlements become candidates for `AbandonedZone` reclassification.
4. **Landmark naming:** Named landmarks in minor cities can draw their names from the controlling corporation's name in the economics database. The generator already knows which corporation controls logistics in which system. ISC controlling logistics in a system → its depot is "ISC Logistics Hub," not "Port 7."
### Trade flow intensity → road quality signals
Trade flow data (which routes carry what volume) feeds into:
1. **Trade-maintained roads:** High-flow trade routes get `MaintenanceAuthority = Trade`, producing good road quality even in politically marginal regions. This creates the interesting signal Paula identified: commerce went where administration didn't bother.
2. **FreePort prosperity baseline:** FreePort cities have prosperity_index derived partly from trade volume. High trade flow = higher baseline. This means FreePort cities can have high-prosperity districts even in Backwater WorldTier systems — trade beats administration as a wealth engine.
3. **Contested boundary identification:** When two corporations with conflicting interests both have significant trade flow through the same zone, that zone is a candidate for `ContestZone` or elevated `perimeter_treatment` at district level.
### The economics feedback loop at district level
The economics bridge is not one-directional. Here is the feedback path:
```
systems.db economics data
→ Phase 3: political_archetype, prosperity_index, territorial_status
→ Phase 5 CityGenerationContext
→ DistrictSkeleton: DistrictType distribution, prosperity_index per district
→ Phase 2 tile generation: building density, road width, decay tiles
→ Player reads: "this is a corporate extraction zone, these workers are poor,
there are checkpoints, the road only goes to the mine"
```
The player reads the spatial output of economic decisions without the economics being labeled. This is asymmetric information operating at the generation level, not just the runtime level. The world makes economic sense spatially because the generator was driven by economic inputs.
---
## 3. LoD Cascade — Authored ↔ Derived Spectrum
Every Phase 3 element sits somewhere on this spectrum. Here is the full map:
### Fully Authored (hand-written, changes require wiki edit)
| Element | Who authors | Authoring scope |
|---------|------------|-----------------|
| Planet definitions (bodies table) | Miri | All bodies |
| City names | Mellanie + Miri | All named cities |
| `political_archetype_override` | Wiki authors | Named cities only, when default is wrong |
| `prosperity_override` | Wiki authors | Named cities only, when lore demands |
| Named landmark names in major cities | Miri | Capital cities + named stations |
The authored layer is intentionally small. The system is designed so that wiki authors make high-value lore decisions, not repetitive data-entry decisions.
### Derived from Authored Sources (Python tooling, committed to systems.db)
These are computed deterministically from authored inputs. They live in systems.db and can be regenerated by `make regen-db`:
| Phase 3 Layer | Element | Derivation root |
|--------------|---------|----------------|
| A | `footprint_radius_km` | population + settlement_pattern formula |
| A | `political_archetype` | economic_role mapping |
| A | `prosperity_index` (city baseline) | WorldTier + economic data + trade flow |
| A | `founding_orientation` | settlement_pattern + economic_role |
| B | Regional biome grid (64×32) | planet_simulation terrain downsampled |
| C | Road graph nodes + edges | atlas road pixel-paths snapped to cities |
| C | `maintenance_authority` per road segment | road endpoint types + economics |
| D | `territorial_status` per cell | city proximity × WorldTier + economics |
| E | Station module topology | economic_role + population template |
| F | `land_use` per cell | biome + city footprints + road graph + territorial_status |
| F | Sub-city settlement placement | road graph + economics + WorldTier |
| F | `political_tether` per sub-settlement | distance + economics derivation |
This is the bulk of Phase 3. It requires no ongoing authoring effort — it regenerates correctly when sources change.
### Seed-Derived (Phase 5 runtime, never stored)
| Element | Why seed-derived |
|---------|----------------|
| DistrictSkeleton (all fields) | Function of (district_seed, city_context) |
| District count and positions | Function of (city_data, world_seed) |
| Exact sub-settlement position within cell | Seeded within corridor bounds |
| `DualNaming` common_name content | Seeded from culture pool when conditions met |
| Wilderness interior terrain variation | Seeded below regional resolution |
| Building footprints within districts | Seeded from block skeleton |
### The LoD Cut Between Phase 3 and Phase 5
The key architectural boundary: **Phase 3 describes "what kind of place is this?" Phase 5 generates "what does this place look like tile by tile?"**
Phase 3 never generates walkable space. It generates classification, context, and constraints. Phase 5 reads those constraints and generates the tiles that express them. A city in ExtractiveZone + CompanyTown archetype gets districts whose types, prosperity levels, and perimeter treatments reflect that context — but the actual tile layout is Phase 5's job, seeded deterministically.
---
## 4. What Creates Interesting Decisions at Regional Scale
This is the design question I care most about. Let me break down what "interesting decisions" means at regional scale and what the generator must produce to support them.
### The Decision Types
**Route decisions: multiple paths between settlements**
The player navigating a regional map faces the core traversal question: which way do I go? For this to be a meaningful choice, routes must differ in ways that matter:
| Route characteristic | What the generator must produce |
|---------------------|-------------------------------|
| Speed (road quality) | MaintenanceAuthority drives quality; Corporate roads are fastest, Communal roads follow prosperity |
| Political exposure | TerritorialStatus zone sequence along each route |
| Resource availability | Sub-settlement placement along route; Corridor land use guarantees supply nodes |
| Risk profile | WildernessBuffer / AbandonedZone → no infrastructure safety net |
| Cost (implied) | Trade route vs. administrative road → different access politics |
For this to work, **routes must not be equivalent**. The generator must produce enough settlement hierarchy and road quality variation that two paths between the same cities have meaningfully different character. Paula's `MaintenanceAuthority` + `TerritorialStatus` combination is exactly what produces this.
**Settlement access decisions: what is this place?**
Before entering a settlement, the player should be able to read its political character from the regional map. The signals:
- `PoliticalTether = Corporate` → expect corporate security, corporate pricing, corporate rules
- `PoliticalTether = Independent` → expect local law (or none), opportunistic pricing
- `PoliticalTether = Contested` → expect double-sided checkpoints, unstable rules
- `PoliticalTether = Abandoned` → expect no services, possibly ruins worth investigating
These signals translate to "should I stop here or push on?" decisions with real consequences.
**Risk/reward: wilderness and abandoned zones**
`WildernessBuffer` and `AbandonedZone` regions have no infrastructure — no shelter, no repair, no trade. But they have:
- Ruins (AbandonedZone): historical footprint worth investigating
- Resources (WildernessBuffer): unclaimed, unguarded
- Escape routes (both): off the administered grid, less visibility to authority
The regional map must make wilderness legible as a risk-reward tradeoff, not random emptiness. This requires:
1. Clear visual distinction between zone types
2. Visible road quality degradation at zone entry
3. Distance markers to nearest administered settlement
**Political navigation: zone-crossing as faction exposure**
A player with corporate affiliations faces scrutiny in ContestZone or government CoreTerritory. A player avoiding authorities prefers WildernessBuffer routes despite the cost. The regional map is a political exposure map, not just a physical terrain map. Different players will read it differently based on their current faction relationships.
This is **asymmetric information at the generation level** — the generator knows the full political geography, but the player reads it through the lens of their current situation.
**Founding orientation as navigation aid**
`FoundingOrientation` provides directional information about city structure before entering. A ResourceFacing city has its industrial district on the resource-facing edge — a player who wants to avoid industrial (or specifically seek it) can predict which part of the city to enter from. This is readable from the regional map if the city icon or approach indicates orientation.
### The Decision-Supporting Minimum
For regional decisions to be interesting, the generator must produce at minimum:
1. **Route variety**: ≥2 meaningfully different routes between major settlements (requires road graph + MaintenanceAuthority)
2. **Zone legibility**: TerritorialStatus visible per regional cell on the Atlas UI
3. **Settlement hierarchy**: At least towns and outposts along major routes (sub-settlements in Layer F)
4. **Approach legibility**: FoundingOrientation and political_archetype readable before city entry
Without #1 and #2, the regional map is a lookup table, not a decision space. The player consults it to find cities but doesn't navigate it strategically.
---
## 5. Ticket Dependency Graph for Phase 3
### New tickets required for Phase 3
The dependency chain is layered. I've marked parallelizable branches.
```
NEW: generate_regional.py pipeline scaffolding [0.5d]
│ (Makefile integration, meta stamp, pre-push hook update,
│ --body / --force / --dry-run flags, generator_sha tracking)
├──▶ [A] City classification + footprint [1.5d]
│ (footprint_radius_km, political_archetype, founding_orientation,
│ prosperity_index, override columns on atlas_cities)
│ │
│ ├──▶ [C] Road network graph [1d]
│ │ (atlas_road_nodes + atlas_road_edges tables,
│ │ snap-to-city, distance, maintenance_authority)
│ │ │
│ │ ├──▶ [D] TerritorialStatus zones [1d]
│ │ │ (formula: proximity × WorldTier + econ;
│ │ │ territorial_status column on atlas_regional_biomes)
│ │ │ │
│ │ │ └──▶ [F] Wilderness + settlement hierarchy [1.5d]
│ │ │ (land_use column, atlas_sub_settlements table,
│ │ │ PoliticalTether derivation,
│ │ │ DualNaming conditions)
│ │ │ │
│ │ │ └──▶ CityGenerationContext Rust struct [0.5d]
│ │ │ (read-path only; queries all Phase 3 tables
│ │ │ from systems.db at game startup)
│ │ │ │
│ │ │ └──▶ Phase 5 wiring [0.5d]
│ │ │ (wire CityGenerationContext
│ │ │ into DistrictSkeleton Stage 1)
│ │ │ │
│ │ │ └──▶ [MINIMUM VIABLE REGIONAL LAYER]
│ │ │
│ │ └──▶ Phase 3 Atlas UI (client, stig's ticket)
│ │ [BLOCKED until C complete; can begin with stub data]
│ │
│ └──▶ [E] Station module topology [1.5d]
│ (atlas_station_modules + atlas_station_connections,
│ template-based generator, separate code path)
│ [PARALLEL with C — no dependency between them]
└──▶ [B] Regional biome grid [1.5d]
(atlas_regional_biomes table, 64×32 sampling,
integrated into generate_atlas.py terrain pass)
[PARALLEL with A — different data sources]
└──▶ feeds into [D] (TerritorialStatus uses biome data)
[D cannot complete until both A + B + C are done]
```
### Parallelizable branches
- **[A] and [B]** are fully independent — different data sources, different tables.
- **[E] (station topology)** is independent of B-D. Can start immediately after scaffolding.
- **[C] (road graph)** needs Layer A city nodes, not Layer B biome data.
- **[D] (TerritorialStatus)** needs A + B + C. This is the merge point.
- **[F] (wilderness/settlements)** needs A + B + C + D. This is the final layer.
- **CityGenerationContext Rust struct** can begin development in parallel with Phase 3 tooling, stubbing out values, and wire to real systems.db data when Phase 3 is complete.
### Effort summary
| Ticket | Effort | Dependencies |
|--------|--------|--------------|
| generate_regional.py scaffolding | 0.5d | None |
| [A] City classification + footprint | 1.5d | Scaffolding |
| [B] Regional biome grid | 1.5d | Scaffolding (parallel with A) |
| [C] Road network graph | 1d | Layer A |
| [D] TerritorialStatus zones | 1d | Layers A + B + C |
| [E] Station module topology | 1.5d | Scaffolding (parallel with B-D) |
| [F] Wilderness + settlement hierarchy | 1.5d | Layers A + B + C + D |
| CityGenerationContext Rust struct | 0.5d | Layer A (for struct shape); full data after F |
| Phase 5 wiring | 0.5d | CityGenerationContext + Phase 5 Phase 1 milestone |
| **Phase 3 total** | **~9.5d** | — |
| Phase 5 (from Round 2) | ~6d | CityGenerationContext stub values |
**Critical path (sequential only):**
Scaffolding → A → C → D → F → CityGenerationContext → Phase 5 wiring = ~5.5d
Phase 3 and Phase 5 can be developed in parallel. Phase 5 develops against stub `CityGenerationContext` values. After Phase 3 lands, Phase 5 wiring ticket wires the real data.
### Relationship to existing tickets
| Ticket | Phase 3 impact |
|--------|---------------|
| #899 (rescoped Phase 1 Stages 1-2) | Must consume `CityGenerationContext`; add dependency on wiring ticket |
| Phase 3 Atlas UI (new) | Client-side; reads tables Phase 3 produces; assign to stig |
| generate_regional.py as generator | Add to `GENERATOR_SOURCES` in `tooling/check-systems-db-stamp`; update `/pr-push` source watch list |
---
## 6. Positions on Round 2 Open Questions (Already Resolved by Tyre R3)
Tyre answered all Round 2 open questions. My positions align:
- **OQ-R2-1 (prosperity_index and perimeter_treatment in minimum slice):** YES. Include before Phase 2 is implemented. Retrofit cost > add-now cost.
- **OQ-R2-2 (political_archetype — formal field or implicit):** Formal field in atlas_cities. Derived by default. Override allowed. The district generator receives a value, does not compute lore classifications.
- **OQ-R2-3 (tile algorithm — door-per-block-edge vs. density-driven rectangles):** Both, in order. Street skeleton first (door-per-block-edge), then building fill (density-driven rectangles within that skeleton).
- **OQ-R2-4 (Step 2b full city layout):** Follow-on ticket. Data model must support N districts from day 1.
- **OQ-R2-5 (naming register before Phase 2):** Post-walkable. Naming is text overlay, not tile-generation prerequisite.
- **OQ-R2-6 (prosperity_index authored or derived for named cities):** Derived by default, `prosperity_override` column for wiki authors.
- **OQ-R2-7 (naming register ownership):** Paula proposes taxonomy, Mellanie populates name pools, Tyre reviews for spatial contradictions, lead locks as D-record.
---
## 7. Open Questions for Burnelli-Sheldon
The economics bridge in §2 is based on my reading of what economic data should drive spatial structure. Burnelli-Sheldon should review and fill gaps:
| ID | Question | Priority |
|----|----------|----------|
| OQ-R3-G1 | Does the economics data in systems.db have sufficient granularity to derive `TerritorialStatus` per regional cell? Specifically: can I tell from existing data whether a corporation has active presence in a given 8×8-pixel zone, or only at the body level? | **HIGH** |
| OQ-R3-G2 | Trade flow intensity: is this derivable from existing systems.db supply chain and brand data, or does it require additional economics simulation? The road quality signal depends on this. | **HIGH** |
| OQ-R3-G3 | Corporate extraction zones: is `economic_role = "corporate_extraction"` at the body level sufficient to identify ExtractiveZone regions, or do I need sub-body-level economic geography? | HIGH |
| OQ-R3-G4 | FreePort prosperity baseline: my proposal is that FreePort cities derive prosperity from trade flow intensity rather than WorldTier. Does Burnelli-Sheldon agree with this economically? A Backwater transit hub should be prosperous in ways that a Backwater administrative center is not. | MEDIUM |
| OQ-R3-G5 | When a corporation's economics data shows it no longer operating in a body (abandoned extraction), should the sub-settlements auto-reclassify to AbandonedZone, or does this require an explicit authored flag? | MEDIUM |
---
## 8. Summary: What Phase 3 Is and Isn't
**Phase 3 is:**
- A tooling phase producing enriched systems.db data
- The layer that gives Phase 5 district generation its political and spatial context
- The generator for the Atlas of the Reach implant app (Phase 3 deliverable)
- ~9.5 developer days of Python tooling + Rust read-path work
- Parallelizable with Phase 5 (Phase 5 stubs out with CityGenerationContext values)
**Phase 3 is not:**
- Walkable tile generation (that is Phase 5)
- Heritage grammar, NPC population, social sites (out of scope)
- Room templates or building interiors (out of scope)
- Real-time computation (all Phase 3 data is offline, committed to systems.db)
**The minimum Phase 3 for Phase 5 to function:**
- Layer A: City classification (political_archetype + prosperity_index + founding_orientation)
- Layer C: Road graph (road_entry_directions per city)
- Layer B: Regional biome grid (surrounding biome context)
Layers D, E, F enrich Phase 5 and the Atlas UI but are not required for the first walkable district. This gives us a sequencing option: deliver minimum Phase 3 (A + B + C) to unblock Phase 5, then complete D + E + F in parallel with Phase 5 development.
---
*Gestalt — Round 3. Written 2026-04-30.*
@@ -0,0 +1,468 @@
---
title: "Gestalt — Round 4: Systems Requirements for the Planet-Down Cascade Brief"
description: "Systems design questions, game-mechanical decisions, interesting-factor analysis, resolved carry-forwards, and minimum viable scope for each of the four cascade layers"
type: workshop
status: active
workshop: generation-cascade
agent: gestalt
round: 4
created: 2026-04-30
---
# Generation Cascade — Round 4 (Gestalt)
**Role:** Systems design requirements for the next workshop brief. This document does not design the implementation — it frames what the implementation must answer mechanically, what each layer's output means for gameplay, and what we already know well enough not to relitigate.
**The lead's cascade reframe:** Planet-down, not city-outward. The world exists before civilization. Civilization is layered onto it. The player enters at the bottom of that stack and reads upward.
**The two rules that govern everything:**
> **DETERMINISM RULE:** Economic simulation's rolling state affects RENDERING (prosperity, repair state) but NOT LAYOUT (streets and buildings locked by seed). A player who leaves a city and returns a year later finds the same streets, richer or poorer.
> **LOD RULE:** When a player spawns, the code "zooms in" on the planet — tracing all cascade steps to where the player sits, filling in street-level detail where the player is. Each layer's output is the next layer's input.
These two rules are the architectural spine. Every system design question below ultimately reduces to: does this belong in seed-locked layout, or economics-variable rendering?
---
## The Sub-Settlement Hierarchy (Lead Inclusion)
Before the layer-by-layer breakdown: the lead has confirmed sub-settlements are included. This has systems-design implications across all four layers, so I'm stating the hierarchy here and referencing it throughout.
From the Round 3 consensus (Burnelli-Sheldon + Paula + Gestalt):
| Tier | Population | Generation treatment | Placement driver |
|------|-----------|---------------------|-----------------|
| City | 10,000+ | Full district decomposition (Layer 3) | markers.json anchor + geography |
| Town | 1,00010,000 | Single-district equivalent | Road network + economic role |
| Outpost | 501,000 | Purpose-typed single node | Corp presence + extraction trigger |
| Waypoint | <50 | Infrastructure node, no district | Road midpoint formula |
| Rural cluster | Isolated | Type-tagged scattered nodes | settlement_pattern + economic role |
| Ruin | 0 | Historical footprint | Abandoned tether condition |
**Placement trigger conditions (from Burnelli-Sheldon Round 3):**
- **Mining camps:** `economic_role ∈ {extraction, mining}` AND corp_presence with relevant commodity → 1 camp per 2 qualifying corps, 1 district (LogisticsHub + minimal Residential)
- **Trade waypoints:** Long roads (pixel_count > threshold) → waypoint at geometric midpoint, 1 Transit district
- **Agricultural nodes:** `settlement_pattern ∈ {dispersed, dispersed_rural}` AND `economic_role = agricultural` → farm clusters at interval proportional to `1/economic_tier`
- **Shadow nodes:** `collection_efficiency < 0.6` AND shadow-viable corps → 1 informal MixedUse settlement at city fringe
**PoliticalTether:** Every sub-city settlement has a tether to a city (Administered / EconomicallyDependent / Contested / Independent / Corporate / Abandoned). This determines road maintenance class, naming register, and ruin candidacy. Derivation: distance + economics data.
Sub-settlements appear in Layer 2 (placed onto the natural world) and are consumed by Layer 3 (if they receive district treatment) and Layer 4 (if the player approaches tile range).
---
## The Economics Bridge (Burnelli-Sheldon Round 3)
The 6-field minimum read set and the weight table are foundational. I'm stating them here explicitly so the brief can reference them without recapping.
### The 6-Field Minimum Read Set
All six fields are queryable from systems.db per city at generation time. They produce a spatially differentiated world on their own. Everything else is enrichment.
| # | Field | Source table | Drives |
|---|-------|-------------|--------|
| 1 | `economic_role` | `bodies` | DistrictType distribution weights (weight table below) |
| 2 | `settlement_pattern` | `bodies` | District density mode, layout style, domed/underground special cases |
| 3 | `economic_tier` | `system_economy` | Infrastructure quality floor, wealth ceiling |
| 4 | `distribution_index` | `system_economy` | Prosperity gradient shape (stratified vs. moderate) |
| 5 | `corp_presence` count | `corp_presence` | Corporate district intensity, company town detection |
| 6 | `headquarters_system/body` match | `corporations` | HQ landmark flag, dominant building height |
### The 10×9 DistrictType Weight Table (D-C10 candidate)
Probability weights mapping `economic_role` → DistrictType prior. Corp-presence modifiers stack on top. This is the canonical default — tunable but locked so it doesn't get relitigated.
| economic_role | Res | Com | Ind | Adm | Log | Ent | Mix | Trn | Spe |
|--------------|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| manufacturing | 25 | 5 | 35 | 5 | 20 | 2 | 5 | 3 | 0 |
| agricultural | 30 | 15 | 5 | 10 | 25 | 3 | 10 | 2 | 0 |
| extraction | 20 | 5 | 30 | 5 | 30 | 0 | 5 | 5 | 0 |
| transit | 10 | 20 | 5 | 5 | 25 | 10 | 15 | 10 | 0 |
| research | 20 | 5 | 5 | 15 | 5 | 5 | 10 | 0 | 35 |
| commercial | 15 | 35 | 5 | 5 | 10 | 15 | 15 | 0 | 0 |
| service_mixed | 25 | 20 | 5 | 10 | 5 | 15 | 20 | 0 | 0 |
| mining | 20 | 5 | 25 | 0 | 35 | 0 | 10 | 5 | 0 |
| frontier | 35 | 10 | 10 | 5 | 20 | 5 | 15 | 0 | 0 |
| energy | 10 | 0 | 20 | 5 | 30 | 0 | 5 | 5 | 25 |
`political_archetype` modifiers apply on top of these weights to shape district type *distribution*: CompanyTown amplifies the dominant type; FreePort flattens toward MixedUse/Transit/Commercial; Contested produces discontinuous spatial blocks.
---
## Layer 1 — Empty World
### What This Layer Produces
Start from the heightmap. Run water simulation (river drainage, lakes, coastlines). Refine sub-biomes into regional character. Output: a complete natural world with no civilization.
The existing pipeline already produces a 512×256 heightmap per body and a 64×32 regional biome grid (Layer B from Round 3). Layer 1 extends this with:
- **River network** at regional resolution: where major rivers run, where they meet, where they reach the sea. River confluences and river mouths are settlement attractors for Layer 2.
- **Lake placement** from biome/river basin dependency
- **Sub-biome refinement** within each regional cell: a "tropical" cell is "jungle with clearings" vs. "swamp" vs. "riverine forest" — 3-4 variants per biome class, seed-derived
- **Terrain roughness signals** at regional resolution: pass locations through mountain ranges, traversable vs. impassable zones
### Systems Design Questions for the Brief
1. **At what resolution does the river network get stored?** The 64×32 regional grid gives coarse river paths. If rivers are future walkable obstacles (Phase 5), their exact courses need to be at a resolution the tile generator can use. But if rivers are navigation constraints only at regional scale, the coarse grid is sufficient. **This is a scope question, not a design question — the brief needs to state which.**
2. **What is stored vs. seed-derived for Empty World data?** The heightmap and 512×256 biome grid are committed artifacts from generate_atlas.py. The 64×32 regional grid is committed (Layer B, Round 3). River network routing below regional resolution is candidate for seed-derivation (same FNV-1a pattern). **But river confluence locations (settlement attractors for Layer 2) must be stored or at least deterministically queryable before Layer 2 runs.** This is the key architectural question.
3. **How do sub-biome variants affect generation downstream?** A "dense forest" cell requires terrain modification (clearing) before a settlement exists; a "grassland" cell does not. The Empty World layer needs to produce a signal that Layer 2 can act on. Concrete proposal: `terrain_modification_cost: f32` per regional cell — how much effort is required to settle here. High cost = forest/hills; low cost = plains/coast.
4. **Where do geographic bottlenecks get identified?** Mountain passes, river crossings, and coastal access points are constraint features that later layers (roads, settlements) need to route around or through. These need to be extractable from the Empty World layer as named spatial features, not just implicit in the elevation data.
### Game-Mechanical Decisions This Layer Produces
| Feature | Player decision created |
|---------|------------------------|
| River network (regional) | Route choice: ford vs. bridge; upstream vs. downstream travel |
| Mountain passes | Overland route selection: fast valley route vs. hard mountain pass |
| Coastal access | Settlement approach: sea route vs. overland; port city vs. inland city |
| Sub-biome character | What to expect in wilderness between settlements; terrain hazard prediction |
| Lake positions | Dead-end vs. through-lake navigation; freshwater resource |
| River confluences | Settlement density prediction: confluences attract cities → player plans route accordingly |
The key mechanic: **the player reads terrain to predict settlement density and route quality before seeing a map label.** A river confluence with coastal access is almost certainly a city. A mountain range with a single pass is almost certainly a chokepoint. Empty World teaches the player to read the world.
### What Makes This Layer Interesting for Gameplay
The Empty World is the legibility layer. It explains why civilization sits where it does.
A player navigating from one city to another doesn't just move through abstract distance — they cross terrain that was there before the cities existed and will be there if the cities collapse. The terrain creates:
- **Forced routes:** A mountain range with one pass means all overland traffic converges there. That convergence is interesting — high encounter probability, possible chokepoint control, valuable for whatever faction controls it.
- **Natural shelter:** A river valley that widens into a delta is obviously where the port city is. The player arriving for the first time can orient themselves without a tutorial.
- **Risk gradient:** Moving away from rivers and coast into wilderness means moving away from the infrastructure support network. The player knows this before it's confirmed.
The ABSENCE of civilization in the Empty World layer is as informative as presence. A wide fertile valley with no settlement asks: why? That question is a gameplay hook.
### Resolved Decisions Carrying Forward
- FNV-1a SeedChain (D-010, D-C6): river routing below regional resolution is seed-derived using the same pattern as district/chunk generation
- The 64×32 regional biome grid (Layer B, Round 3) is the base; Layer 1 refines it but does not replace it
- `is_coastal`, `water_fraction`, `terrain_roughness` on `atlas_regional_biomes` already exist — Layer 1 populates `sub_biome_variant` on the same table
- Heightmap is committed artifact from generate_atlas.py — Layer 1 consumes it, does not regenerate it
### Minimum Viable Layer 1
- Sub-biome variant tag per regional cell (3-4 values per biome class): high vs. low modification cost
- River confluence identification (stored as point features in systems.db): "this regional cell has a confluence"
- Coastline identification: already done (`is_coastal` on `atlas_regional_biomes`)
- Mountain pass identification: regional cells with very high `terrain_roughness` adjacent to lower cells = candidate pass locations
- Defer: tile-resolution river routing, lake exact polygon, sub-regional terrain variation
---
## Layer 2 — Population Overlay
### What This Layer Produces
Layer economics onto the natural world. Settlement positions anchor from wiki population counts + markers.json, but **the shape of civilization is determined by geography.** Felled forests become farmland; flattened hills become towns. Road and rail networks connect settlements. Sub-settlement hierarchy placed.
This is the layer where the world becomes inhabited. It does not produce the internal structure of cities (Layer 3) or the walkable tiles (Layer 4) — it places civilization in the landscape and defines the relationships between settlements.
### Systems Design Questions for the Brief
1. **Does "geography determines shape" mean we re-evaluate atlas city positions?** The current model places cities as authored dots in markers.json. If geography determines shape, does this mean: (a) city positions stay but the generator explains/validates them against geography, or (b) the generator produces city positions from geographic attractors and uses markers.json only as population counts? This is a foundational framing question the brief must answer. My read of the lead's intent: **the positions are anchored, but the generator understands WHY they're there** — which then informs how each city sits in its landscape and where sub-settlements cluster.
2. **What is the "terrain modification" record?** The lead says "felled forests become farmland, flattened hills become towns." This means the Empty World's terrain is visibly modified by civilization. Where is this stored? Options:
- Layer 2 commits a `terrain_modification` table to systems.db (what was here before, what it became)
- Layer 2 produces a `land_use` overlay on the regional grid (already in Layer F from Round 3)
- Modification is implicit in the `land_use` classification (Urban = was something else; Agricultural = was forest/grassland)
The simplest answer: the existing `land_use` column (Urban / Agricultural / Industrial / Wilderness / etc.) on `atlas_regional_biomes` IS the modification record. No separate table needed.
3. **How are hinterland agricultural zones shaped?** Burnelli-Sheldon's `HinterlandType` and `InfrastructureDensity` give us the character of the space between cities. But the SHAPE of farmland needs a placement rule. Proposal: agricultural cells cluster around cities with `economic_role = agricultural` and along river corridors, at a density inversely proportional to `terrain_modification_cost` from Layer 1. Rough terrain stays wilderness; gentle terrain becomes farmland.
4. **What is the road placement algorithm?** Currently, markers.json has authored road pixel-paths. The Population Overlay phase is supposed to generate road networks. Are these the same roads (authored once, never regenerated) or a new algorithm that derives roads from settlement positions? **This is a key question for the brief to resolve.** My position: authored roads for named highway routes (these encode setting decisions — Gate Corp built this highway, not the settlement network); generated roads for local connections between sub-settlements and their tether cities.
5. **Sub-settlement exact placement:** Given a body with `economic_role = extraction` and 3 qualifying corps in `corp_presence`, where exactly do the 1-2 mining camps sit? The brief needs to specify the placement algorithm. Proposal: mining camps sit at the closest regional cell to the corp's asset that has `land_use = Industrial` or `land_use = Wilderness` (not Urban), at seeded distance from the tether city within [15%40%] of body scale.
### Game-Mechanical Decisions This Layer Produces
| Feature | Player decision created |
|---------|------------------------|
| Settlement network topology | Route planning: which settlements to visit and in what order |
| Sub-settlement placement | Opportunity discovery: finding the camp before knowing it's there |
| Hinterland agricultural extent | Resource geography: where food comes from, where it's vulnerable |
| Road connectivity gaps | Risk/reward: unconnected settlements are accessible but unprotected |
| Terrain modification extent | Historical reading: how long has this settlement been here? |
| PoliticalTether relationships | Faction awareness: is this outpost administered or corporate? |
The key mechanic: **the settlement network topology creates the player's opportunity map.** Connected settlements are safe but competitive. Disconnected ones are risky but potentially profitable. The gaps in the road network are where interesting things happen.
### What Makes This Layer Interesting for Gameplay
Population Overlay is where the world becomes a problem space. The empty world had terrain; now it has places to go and reasons to go there.
Three specific interesting structures this layer produces:
1. **The underserved route:** Two significant settlements with no direct road — only a wilderness crossing or a long detour through a third settlement. This is a player opportunity (carry trade between them directly) that the generator produces from the absence of infrastructure, not its presence.
2. **The extraction hub:** A cluster of mining camps orbiting a tether city at increasing distance. The outermost camps are the least administered, most economically exploitable, most isolated. The player can read this structure from the regional map and choose their engagement point.
3. **The abandoned corridor:** An old road leading to ruins — AbandonedZone status, former PoliticalTether now severed. The road's quality (Abandoned MaintenanceAuthority) signals that something was here. This is generated entirely from economic data (corporation left) without authored content.
### Resolved Decisions Carrying Forward
- Burnelli-Sheldon's 6-field minimum read set: drives hinterland type and settlement character
- Dynamic settlement trigger conditions (Round 3): mining camps, trade waypoints, agricultural nodes, shadow nodes
- PoliticalTether struct (Round 3): every sub-city settlement classified
- `land_use` column on `atlas_regional_biomes`: Urban / Agricultural / Industrial / Wilderness / Corridor / Ruins / Ocean / Impassable
- `MaintenanceAuthority` on road edges: Administrative / Corporate / Communal / Trade / Abandoned
- `TerritorialStatus` per regional cell: CoreTerritory / FrontierTerritory / ExtractiveZone / ContestZone / WildernessBuffer / AbandonedZone
### Minimum Viable Layer 2
- `land_use` on all regional cells (already in Layer F design from Round 3)
- Sub-settlement placement: mining camps and trade waypoints (highest frequency; most game-relevant)
- Road graph (already designed in Layer C, Round 3): nodes + edges with `maintenance_authority`
- `TerritorialStatus` per regional cell (Layer D from Round 3)
- Defer: agricultural node exact positions (land_use tag is sufficient for minimum), shadow node placement (needs collection_efficiency data quality improvement), authored road vs. generated road resolution
---
## Layer 3 — City-Level Planning
### What This Layer Produces
Given topography + economics + cultural parameters: plot the internal structure of cities. Suburbs, industrial zones, commercial districts, ports, stations, highways. The entire phase. This is the "Cities Skylines" layer.
This maps directly to the DistrictSkeleton work from Rounds 1-3, extended to include the city-scale spatial organization (not just individual district classification). The key addition over prior workshop work: this phase knows WHERE districts sit relative to each other, relative to the city's geographic position, and relative to the settlement's orientation.
### Systems Design Questions for the Brief
1. **What is the City Planning layer's exact output?** I propose it is the `DistrictPlacement` vector + `DistrictSkeleton` (Stages 1-2) for every district in every city. This means Layer 3 = the city decomposition + Phase 1 district skeleton generation from Rounds 1-3. The workshop brief should confirm this equivalence explicitly so implementation teams don't design two separate systems.
2. **How does FoundingOrientation constrain district placement?** A PortFacing city has its logistics/transit districts on the water-edge. An AdminFacing city has its administrative district elevated/central. The district decomposition formula (D-C4) places N districts in a grid — but that grid should be oriented by FoundingOrientation. This is a spatial constraint that the current city decomposition formula does not include. **The brief needs to specify whether FoundingOrientation modifies the district placement grid, or just the prosperity gradient.**
3. **What drives the arterial road layout within the city?** The district grid implies roads at district boundaries, but not internal arterials. In Cities Skylines terms: we zone the land (district type assignment) but we also lay the major roads first. The brief should clarify whether internal arterials are:
- Implicit in the district boundary positions (streets exist at every district edge — sufficient for minimum viable)
- Explicitly generated as an arterial graph (more realistic but more complex)
4. **How does topography (Layer 1) constrain city planning?** A city built into a hillside has different arterial possibilities than one on a plain. The brief should specify whether Phase 3/Layer 3 has access to terrain roughness at city-local resolution, or whether it treats all cities as flat for the minimum slice.
5. **Port/spaceport positioning:** Burnelli-Sheldon proposed Transit and LogisticsHub districts at the city edge closest to the orbital station (if one exists). This requires knowing which direction the orbital station is from the city's geographic position. The brief needs to confirm: does Layer 3 receive directional information about nearby stations?
6. **How does the political archetype shape the district placement pattern, not just distribution?** From Paula (Round 3): CompanyTown has a SPINE (facility at one end, workers radiating back); AdminCapital has a CENTER (administrative hub with residential radiating outward); FreePort has MULTIPLE NODES (no single center). These are spatial arrangements, not just type frequency distributions. The brief needs to specify whether Layer 3 implements these arrangement patterns, or whether they emerge from the weight table without explicit arrangement logic.
### Game-Mechanical Decisions This Layer Produces
| Feature | Player decision created |
|---------|------------------------|
| District type layout | Navigation: where to go for what purpose |
| Perimeter treatment distribution | Access route choice: Checkpoint vs. Open approaches |
| Prosperity gradient direction | Economic reading: which end of the city is wealthy/poor |
| Political archetype spatial arrangement | City legibility: player learns CompanyTown vs. AdminCapital patterns |
| FoundingOrientation | Approach prediction: player knows which end has the port |
| Corporate district positions | Faction geography: where to find/avoid corporate presence |
| Port district placement | Entry/exit: where ships dock, where the city opens to outside |
The central mechanic here is **city legibility as player skill.** A player who understands the political archetypes, WorldTier patterns, and economic roles can walk up to a city they've never visited and make accurate predictions about its internal structure before entering. This is meaningful gameplay — the player's knowledge of the system is a form of power.
### What Makes This Layer Interesting for Gameplay
City planning is where systemic knowledge pays off. This layer is interesting not because of individual cities but because of what patterns the system reliably produces.
**Legibility patterns the player learns:**
- CompanyTown: one huge industrial/logistics section, worker housing packed behind it, no entertainment, checkpoint at the facility edge
- FreePort: transit and commerce everywhere, no dominant center, multiple competing entry points, high perimeter treatment variance (some Gated compounds next to Open street markets)
- AdminCapital: wide boulevards converging on a central elevated point, residential quality degrades outward, strong checkpoint presence at city limits but relatively open inside
**Why this is interesting:** Each archetype creates a different navigation and interaction challenge. The player's tactical approach to a CompanyTown is different from their approach to a FreePort. This differentiation is generated, not scripted — the same city archetype produces consistent behavior across every city of that type, which makes player investment in understanding patterns worthwhile.
**The determinism rule's role here:** The city layout never changes. The player can memorize it. This transforms city navigation from a puzzle (solve it once) into a familiar space (navigate it repeatedly with increasing fluency). The economics affecting rendering means the player sees the city get richer or poorer over time while the bones remain recognizable.
### Resolved Decisions Carrying Forward
- District count formula (D-C4): `max(1, floor(population / 50_000))`, log-scaled, WorldTier ceiling
- WorldTier district ceiling: Epicenter uncapped, Regional 8, Backwater 4, Passage 2, Waypoint 1
- City-local coordinate system (D-C5): districts at `(col × 512, row × 512)` sim tiles
- Phase 1 Stages 1-2 minimum (Round 2 consensus): classification + block grid only
- `prosperity_index` and `perimeter_treatment` on DistrictSkeleton (confirmed all participants)
- `political_archetype` formal field with override (confirmed all participants)
- `FoundingOrientation` drives prosperity gradient direction (Paula/Gestalt Round 3)
- 6-field minimum read set and 10×9 weight table (Burnelli-Sheldon Round 3)
- `prosperity_index` derivation formula (Burnelli-Sheldon Round 3):
```
base = economic_tier / 5.0
role_modifier: extraction -0.1, research +0.15, service_mixed +0.1, frontier -0.2 ...
if stratified: district_i = base + role_modifier + (prestige_rank(i) - 0.5) × 0.7
if moderate: district_i = clamp(N(base + role_modifier, 0.1), 0.2, 0.8)
```
### Minimum Viable Layer 3
- Phase 1 Stage 1: classification (WorldTier, ComplexityTier, SettingType, DistrictType, LayoutMode)
- Phase 1 Stage 2: block grid (4×4 BlockSkeletons with ZoningType, density_pct, seed)
- `prosperity_index` and `perimeter_treatment` computed in Stage 1 from the 6-field read set
- `political_archetype` consumed from systems.db (Layer 2 output)
- Defer: FoundingOrientation-modifying district placement grid (Stages 3-5, guarantee audit, reservations, social sites)
- Defer: explicit arterial road layout within city (district boundary = implicit street)
---
## Layer 4 — Street-Level Rendering
### What This Layer Produces
Take city-level data (Layer 3). Render streets, buildings, scatter civilization into tiles. The player walks through this.
Mechanically: generate 64×64 tile grids (ChunkData) from BlockSkeletons, on demand as the player moves.
### Systems Design Questions for the Brief
1. **Precisely what does the DETERMINISM RULE mean at implementation level?**
The lead says: economic simulation's rolling state affects RENDERING but NOT LAYOUT. The brief needs to make this concrete:
| Category | Seed-locked (never changes) | Economics-variable (changes per tick state) |
|----------|---------------------------|---------------------------------------------|
| **Layout** | Street positions, building footprints, door positions, room layouts, district boundaries | — |
| **Rendering** | — | Tile decay state (intact → cracked → broken), building activity signals (lights on/off, signs present/absent), repair scaffolding presence |
Implementation proposal: **the tile generation function produces a canonical tile type** (floor_street, floor_interior, wall). The economics rendering pass then applies a **decay overlay** — each canonical tile has a condition modifier (`intact | worn | cracked | damaged | rubble`) derived from `prosperity_index` at render time, not at generation time. The seed generates the layout; the economics tick generates the condition.
2. **When exactly does Street-Level fire in the "zoom in" cascade?** The existing chunk streaming system (`load_chunk()`) fires when the player's `ChunkLoadRadius` includes a chunk coordinate. This is already the LoD trigger. The brief should confirm: **Layer 4 runs on-demand inside the existing chunk streaming architecture, not as a separate batch process.** No new trigger mechanism needed; the hookup ticket from Round 2 is the wiring.
3. **What is the minimum tile vocabulary that supports economics-variable rendering?** The Round 2 minimum was 3 tile types: `floor_street`, `floor_interior`, `wall`. For the determinism-plus-rendering model, we need tile variants or a condition modifier system. Proposal: keep 3 canonical types; add a condition enum (`Intact | Worn | Cracked | Broken`) that the economics rendering pass applies. This is 4 condition states × 3 tile types = 12 visual variants, but only 3 generation types.
4. **Where does the economics rendering pass execute?** Options:
- At chunk generation time (bake prosperity_index into tile conditions when the chunk first generates)
- As a live overlay per tick (tile conditions computed from current prosperity each time the chunk is rendered)
- As a periodic re-render (chunk conditions recomputed when prosperity changes significantly)
The determinism rule implies the LAYOUT is baked at generation time, but tile CONDITIONS should not be baked — they need to reflect current economic state. **The brief needs to specify the condition update mechanism.**
5. **Building interiors: when do they generate?** Door-per-edge is the confirmed boundary. The brief should specify what triggers interior tile generation: (a) when the player is within threshold of the door, (b) when the player opens/enters the door, or (c) pre-generated with the exterior but not rendered until entered. Performance implications differ significantly.
6. **What is "scatter civilization into tiles"?** The lead's phrasing suggests more than just floor/wall. Does this include: prop placement (furniture, vehicles, crates), surface decals (signage, paint), entity spawn points? These are distinct from tile types. The brief needs to establish what Street-Level Rendering is responsible for vs. what is runtime placement by the entity system.
### Game-Mechanical Decisions This Layer Produces
| Feature | Player decision created |
|---------|------------------------|
| Walkability (tile positions) | Pathing: which routes through a district are possible |
| Building footprint positions | Navigation: where doors are, which buildings block line of sight |
| Door-per-edge thresholds | Interior access: which buildings can be entered |
| Street connectivity | Approach routing: dead-ends, through-routes, alleys |
| Tile condition (economics) | Reading the district: visible decay signals current prosperity |
| Density variation | Area character: packed warehouse district vs. open boulevard |
The central mechanic: **the street level is where the player has physical presence.** All decisions above this layer were about where to go and why. This layer is about how.
### What Makes This Layer Interesting for Gameplay
Street-Level Rendering is where the player's understanding of the system becomes tactile.
**The determinism dividend:** Because layout is seed-locked, every visit to a city produces the same physical space with potentially different economic conditions. This creates two distinct player experiences:
1. *First visit:* Exploration — finding routes, locating key buildings, understanding the district's arrangement
2. *Return visit:* Operational — applying remembered knowledge to new conditions ("The Checkpoint guard post is in the same place, but the district looks richer than last time")
The player's spatial memory becomes an asset. Cities reward familiarity. This is a different and richer relationship with procedurally generated space than "different every time."
**The economics-variable rendering creates observable history.** A district in decline shows decay tiles accumulating over time. A district being developed shows repair states. The player who visits frequently sees the arc; the player who returns after absence sees a snapshot difference. Neither requires exposition — it's written into the tiles.
**Sight lines as political geometry (Paula Round 2 principle, applicable here):** High-prosperity/Checkpoint districts have open corridors — surveillance optimization creates navigable sightlines. Organic/low-prosperity districts have broken sightlines — buildings jut into streets, alleys angle away. This is a tile placement rule, not a separate computation: Checkpoint + Grid layout → buildings placed to maintain corridor sightlines; Organic + low prosperity → buildings placed to break them. The player navigating these districts has different information horizons.
### Resolved Decisions Carrying Forward
- `GeneratorChunkData` upgrade: `Vec<bool>` → `Vec<TileEntry>` with `tile_id: TileId` + `walkable: bool` (D-C7)
- 3 minimum tile types: `floor_street`, `floor_interior`, `wall` (Round 2 consensus)
- Phase 2 minimal tile generation: density-driven rectangle placement (Tyre Round 2 algorithm)
- FNV-1a SeedChain: `chunk_seed = child_seed(district_seed, (cx << 32 | cy))` (D-C6)
- 64×64 tiles per chunk, 2×2 chunks per block, 4×4 blocks per district (existing architecture)
- Door-per-edge building interiors: boundary confirmed; descriptor + catalog behind it (CLAUDE.md)
- Phase 2 tile generation algorithm: street skeleton first (door-per-block-edge), then building fill (density-driven rectangles within skeleton) — Tyre/Gestalt Round 2 consensus
- Chunk streaming → generator wiring: `DistrictMap` resource + `load_chunk()` hookup ticket (Round 2 design)
### Minimum Viable Layer 4
- 3 tile types generating a walkable, navigable space
- Seed-locked layout from `district_seed` via FNV-1a
- Chunk streaming hookup: `load_chunk()` calls `generate_chunk_tiles()` via `DistrictMap`
- Prosperity affects building density (already in Phase 2 algorithm — high density at low prosperity, low density at high prosperity)
- Defer: tile condition overlays (economics-variable rendering) — implement after minimum walkable world exists
- Defer: building interior generation beyond door-per-edge boundary
- Defer: prop/decal scatter
---
## Cross-Layer Systems: The Two Rules Applied
### Determinism Rule — Boundary by Layer
| Layer | Seed-locked (generated once, never changes) | Economics-variable (changes per tick state) |
|-------|--------------------------------------------|--------------------------------------------|
| Layer 1 (Empty World) | Heightmap, river network, biome grid, pass locations | Nothing — terrain is fixed |
| Layer 2 (Population) | Settlement positions, road topology, sub-settlement placement, PoliticalTether | Road condition (Abandoned roads could degrade further — edge case) |
| Layer 3 (City Planning) | District positions, district types, block grid, prosperity_index baseline, perimeter_treatment | prosperity_index current value (rolling simulation updates this); building availability in commercial districts |
| Layer 4 (Street-Level) | Street positions, building footprints, door positions, all tile layout | Tile condition states (Intact/Worn/Cracked/Broken), building activity signals |
**The key implementation note:** `prosperity_index` at Layer 3 is a BASELINE value derived from economic role and WorldTier. The simulation's rolling state produces a CURRENT value. Layer 4 uses the current value for condition overlays, not the baseline. The baseline is stored in systems.db; the current value is runtime economic state. These are two different data fields.
### LoD Rule — Zoom-In Cascade
When a player spawns, the code traces the full cascade from planet level down to the player's tile position. The cascade is lazy — only what the player needs is computed:
```
Planet exists in systems.db (Layer 1 + 2 data committed)
├── Player within planet rendering range?
│ → Activate regional biome overlay for atlas UI
├── Player within city footprint range?
│ → Generate city district grid (Layer 3 / Phase 1)
│ → Build DistrictMap in memory
├── Player within district range (chunk load radius)?
│ → load_chunk() fires for each chunk in radius
│ → Layer 4 / Phase 2 generates tile data from BlockSkeleton + chunk_seed
└── Player at building door threshold?
→ Interior descriptor + catalog loaded (door-per-edge)
→ Interior tiles generated (Phase 4+)
```
Nothing above the player's current resolution is generated at tile level. Nothing below the player's current resolution is generated at all. The cascade is the implementation of the LoD rule.
**One concrete implication:** The `DistrictMap` resource (city district grid) is built when the player enters a city's footprint radius. If two players are in different cities, two separate `DistrictMap` instances exist. If neither player is near a given city, that city has no `DistrictMap` — only its `CityGenerationContext` in the startup read from systems.db.
---
## Open Questions for the Brief
These are not design questions — they are decisions the lead or the brief must make explicit before implementation teams can proceed without ambiguity.
| ID | Question | Layer | Stakes |
|----|----------|-------|--------|
| OQ-R4-G1 | Does "geography determines shape" mean city positions in markers.json are re-evaluated by Layer 2, or do they stay fixed as anchors with Layer 2 explaining them? The answer determines whether `generate_regional.py` can write back to markers.json or only extends systems.db. | 2 | **Architecture-level** |
| OQ-R4-G2 | What exactly does Layer 4's economics-variable rendering produce? The brief should specify: condition enum values, what triggers condition recalculation (prosperity threshold change? periodic tick?), and whether conditions are cached per chunk or computed live per render. | 4 | **Implementation-level** |
| OQ-R4-G3 | Is `prosperity_index` on DistrictSkeleton the BASELINE (from economic role, stored) or the CURRENT (from rolling simulation, runtime)? The brief should use distinct names to prevent confusion in implementation. Proposed: `prosperity_baseline: f32` (Layer 3 output, stored) vs. `prosperity_current: f32` (runtime simulation state). | 3 + 4 | **Data model** |
| OQ-R4-G4 | Does the City-Level Planning layer produce explicit arterial road layout, or are district boundaries the implicit road network? Full arterials require a road placement algorithm; implicit streets require only district grid positions. | 3 | **Scope** |
| OQ-R4-G5 | At what tile-resolution granularity do rivers appear in walkable generation? If rivers are navigable obstacles at street level, the Phase 2 tile generator needs to know where the river is at tile precision. If they're only regional navigation constraints, the 64×32 grid suffices. | 1 → 4 | **Cross-layer scope** |
| OQ-R4-G6 | Does the sub-settlement hierarchy (towns, outposts, waypoints) receive Layer 3 (City Planning) treatment? A town is described as "single-district equivalent" — does it go through the same DistrictSkeleton generation path, or is it a simplified code path? | 2 → 3 | **Implementation scope** |
---
## Summary: What This Document Is Asking For
The planet-down cascade is architecturally elegant. Four layers, clean dependencies, clear deliverables. The systems design questions above are not blockers to the vision — they are the specific implementation decisions that will determine whether each layer actually creates interesting player choices.
The brief that comes out of this round needs to make these things explicit:
1. **The determinism boundary at each layer** — exactly what seed-locks and what economics can update
2. **The stored/runtime distinction for `prosperity_index`** — baseline vs. current, two different fields
3. **The "geography determines shape" question** — anchor or re-derive?
4. **The sub-settlement depth** — which tiers get full district treatment, which get lightweight nodes
5. **River resolution** — regional constraint or walkable obstacle?
6. **Economics-variable rendering mechanism** — condition states, update triggers, caching
Everything else in Rounds 1-3 carries forward cleanly into this new framing. The 6-field economics read set works at Layer 3. The weight table works at Layer 3. The DistrictSkeleton, SeedChain, DistrictMap, ChunkData upgrade — all of it maps directly to the Layer 3/4 boundary. This workshop produced solid foundational work. The planet-down reframe doesn't invalidate it; it gives it the right context.
---
*Gestalt — Round 4. Written 2026-04-30.*
@@ -0,0 +1,413 @@
---
title: "Miri Round 1 — Worldbuilding and Cultural Input Inventory"
description: "Audit of cultural and worldbuilding inputs required at each generation pipeline layer; content gap analysis; ticket dependency observations"
type: workshop
status: active
workshop: generation-cascade
agent: "miri"
round: 1
created: 2026-04-30
---
# Generation Cascade Workshop — Round 1: Miri (Worldbuilder)
**Topic:** Worldbuilding and cultural input inventory for the generation pipeline
**Date:** 2026-04-30
**Focus:** At which layers do cultural inputs enter? What content data is needed? What's missing that would block generation even if code existed?
---
## Preamble
This is an audit, not a design session. I am mapping what exists, what's missing, and where cultural content must be authored before code can run. The architecture is designed — I am not revisiting it. What I am verifying is whether the worldbuilding inputs that architecture requires are actually available.
Short answer: the content situation is better than I expected at the city-naming layer and substantially worse than expected at the district-interior layer. The cultural infrastructure that feeds Phase 1 district skeleton generation is underspecified. Phase 2 chunk fill has zero authored data to work with.
---
## Setting Note — Van Maanen's Star Naming Ambiguity
Before the pipeline audit, I need to flag a setting inconsistency that will surface in every worldbuilding discussion about the primary test case.
D-036 identifies the primary setting as "Van Maanen's Star." In the current wiki, two systems are plausibly this:
- **GJ-35 (Vuurkloof)** — the real-astronomy Van Maanen's Star. DG white dwarf. South African (Cape/Afrikaner) heritage founding community. Geothermal habitation. No Station Sova. Its Calibration Note explicitly identifies it as "Van Maanen's Star, the nearest known solitary white dwarf."
- **GJ-280B (Sova)** — K-type star in the core. Slavic-heritage founding community of construction engineers. Manufacturing and habitat fabrication economy. Contains Station Sova. The GJ-280B wiki stub references D-036 setting details and matches the logistics hub character.
The `cultural-generation-guide.md` and `culture-van-maanens-star.ron` both label the working-class freight district culture as "Vuurkloof cultural profile" / "Van Maanen's Star Culture," suggesting the two names refer to the same cultural root, not the same star. This reading: Vuurkloof (GJ-35) produced the culture that spread outward; Station Sova (GJ-280B) is where that culture manifests in the current game setting.
**The culture RON file `culture-van-maanens-star.ron` is the authoritative content.** It is operational, validated in the voice pipeline, and correctly models the setting. The naming ambiguity is a wiki documentation gap, not a content error. What is needed is a formal `wiki/cultural-groups/van-maanens-star.md` entry that clarifies the relationship and serves as the canonical reference for generator inputs.
**Action needed (content, not design):** Author `wiki/cultural-groups/van-maanens-star.md` using the existing cultural-group template. The content exists across D-036, D-050, `culture-van-maanens-star.ron`, and `cultural-generation-guide.md`. It needs to be assembled into a single canonical page.
---
## Layer-by-Layer Pipeline Audit
### Layer 1: Galactic — System definitions, star data
**Status:** DONE (`systems.db`, wiki star-system pages, ~230+ system index entries)
**Cultural inputs at this layer:** None at generation time. System names, WorldTier, economic profile, faction presence, and settlement age are authored in wiki pages and systems.db. These are the source material for everything downstream.
**What exists:**
- `wiki/star-systems/` — 230+ system directories with `index.md` entries at varying completion levels
- `wiki/star-systems/catalog.md` — system index
- GTTR guide files (gttr-core.md, gttr-north.md, etc.) — narrative characterizations per region
- `systems.db` — canonical economic data per system (populated by `import_economics.py`)
**Content gaps at this layer:**
- Many `index.md` files contain only stub content ("Pending generation"). The narrative character, faction notes, and silence topics are missing for a significant portion of the catalog. These don't block Phase 1 district skeleton generation directly — the required fields (`world_tier`, `economic_profile`) come from systems.db, not from the wiki narrative text. But they block cultural depth at later layers.
**No action required to unblock Phase 1.**
---
### Layer 2: System/Orbital — Body definitions, orbital mechanics
**Status:** DONE (`systems.db`, bodies tables)
**Cultural inputs at this layer:** System WorldTier assignment. This is the top-level simulation fidelity budget that everything downstream inherits.
**What exists:**
- Body tables in systems.db with gravity, atmosphere class, hydrosphere, economy type, settlement wave, industrial focus
- These fields implicitly carry cultural information (settlement wave → settlement age → architectural character)
**Critical gap: No formal system → heritage root mapping.**
The `SocietyProfileRef` field on `DistrictSkeleton` is typed as `String` — a stub. The generator will need to resolve a system to a heritage root blend to fill this field. Example: Van Maanen's Star → `[Iron(0.4), Stone(0.3), Tide(0.3)]`. No authored file establishes these mappings for any system.
This is the upstream content dependency for Phase 1 district social structure. The culture RON files (`culture-van-maanens-star.ron`, etc.) provide NPC voice/behavior data but do NOT include heritage root blend weights. The generator cannot derive heritage blend from the culture file as currently structured.
**Content gap — CRITICAL for Phase 1:** Heritage root blend specification per system is entirely missing. It must be authored before Phase 1 can assign `SocietyProfileRef` to districts with any cultural meaning.
---
### Layer 3: Planetary Surface / Heightmaps
**Status:** DONE (`tooling/planet-gen/planet_simulation.py`)
**Cultural inputs at this layer:** None. Terrain is physically simulated from stellar parameters. Cultural inputs are not relevant at this stage.
**No content gaps from a worldbuilding perspective.**
---
### Layer 4: Atlas Markers — City placement, roads, rail, rivers, mountains
**Status:** DONE (`tooling/planet-gen/generate_atlas.py`, `markers.json` per body)
**Cultural inputs at this layer:** City names, road network character, river/mountain names.
**What exists:**
- `gemma_naming.py` — operational cultural naming pipeline using Gemma 4 via sr-voice
- `naming_core.py` — cultural corridor-aware batch naming with dedup and blocklist
- City markers: `{"id": "city_0", "name": "Glødberg", "kind": "capital", "center": [235, 394], "population": 8200}` — names are populated by Gemma naming
- Earth blocklist (`earth_blocklist.txt`) prevents major real-world cities from appearing
**What is missing from markers.json city entries:**
Current city marker fields: `id`, `name`, `kind`, `center`, `population`. That is all.
Ticket #899 (District skeleton generator — Phase 1 implementation) describes reading `primary_function`, `planet_class`, and `settlement_pattern` from city entries to derive SettingType, ComplexityTier, and DistrictLayoutMode. **These fields do not exist in the current markers.json schema.**
This is a data pipeline gap between Layer 4 (atlas markers) and Layer 6 (district skeleton). Either:
a) The city marker schema needs extending with cultural/functional fields, or
b) Phase 1 derives these from systems.db body/economy data cross-referenced by city position
**Action needed:** Clarify with Tyre which approach is intended. If option (a), define the new city marker fields and update `generate_atlas.py` to populate them. If option (b), document the cross-reference logic. Either way, `markers.json` is currently insufficient for Phase 1 input.
**Cultural corridor definitions** are embedded in `naming_core.py` prompts and not documented anywhere in the wiki. This is a maintainability gap — if a content author needs to understand why a region's names sound the way they do, there is no wiki document to consult. This is not a generation blocker but is a documentation debt.
---
### Layer 5: Economics — Supply chains, corporations, brands, trade flows
**Status:** DONE (`tooling/economy-db/`, `systems.db`, `wiki/economics/`)
**Cultural inputs at this layer:** Corporation identities, brand heritage tags, currency zone characters.
**What exists:**
- `wiki/economics/corporations/` — brand and corporation TOML files
- `wiki/economics/commodities.toml` — commodity catalog
- `wiki/economics/currency_zones.toml` — currency zone definitions
- `wiki/economics/production_chains.toml` — supply chain definitions
**No significant content gaps from a worldbuilding perspective for current Phase 2 economics layer.**
Setting note: the `currency_preference` field on corporations mirrors their HQ booking zone currency — this is consistent with the economics convention documented in memory. Economics layer is healthy.
---
### Layer 6: District Skeleton — Phase 1 (STUB)
**Status:** STUB — data model exists, no generation logic
**Key file:** `server/src/simulation/generator.rs`
**Cultural inputs at this layer (per workshop architecture):**
Phase 1 has 5 stages. Cultural inputs enter at stages 1, 2, and 4:
**Stage 1 — Classification (WorldTier, ComplexityTier, SettingType)**
Cultural input needed:
- SettingType: derived from city `kind` + body economy type (Urban, Station, Agricultural, etc.)
- WorldTier: inherited from system-level data
- ComplexityTier: derived from WorldTier + population + economic function
Data available: systems.db has body economy type and settlement data. City `kind` (capital, settlement, outpost) provides coarse classification.
Gap: No authored mapping rules from `(economy_type, city_kind, world_tier)``SettingType`. These rules need to be defined somewhere for the generator to apply them.
**Stage 2 — Block Grid (DistrictLayoutMode, BlockSkeleton ×16)**
Cultural input needed:
- DistrictLayoutMode: Grid (Commission-planned = power imposed) vs Organic (pioneer = power negotiated)
- Heritage root influences probability: Frost/Arc heritage → Grid bias; Tide/Vine/Dust → Organic bias
Gap: No authored heritage root → layout mode probability table exists. This decision was confirmed (L-2: "some blocks grid, some organic chaos") but the probability weights are not authored.
**Stage 4 — Social Site + NPC (triangles, society profile)**
Cultural input needed:
- `SocietyProfileRef` — references a culture profile for the district
- Heritage root blend — determines social grammar, trust model, observation density, etc.
What exists:
- `server/content/global/culture-van-maanens-star.ron` — COMPLETE culture profile for Van Maanen's Star. Includes naming, speech, values, voice persona, voice examples, behavior modifiers. Operational in voice pipeline.
- `server/content/global/culture-vael.ron` — Core-adjacent, administrative culture (exists)
- `server/content/global/culture-osse.ron` — Deep Frontier, extraction culture (exists)
Gap: Only 3 culture RON files exist for ~230 systems. 99% of systems have no culture profile. Also, culture RON files do not include heritage root blend weights — these are needed by Phase 1 for `DerivedDistrictAnalysis` computation (assassination difficulty, playstyle affinity).
---
### Layer 7: Heritage Grammar Overlay — Phase 2 Chunk Fill (MISSING ENTIRELY)
**Status:** TYPE DEFINED, NO DATA, NO CODE
**Cultural inputs at this layer:** `HeritageGrammarOverlay` per heritage root (10 roots: Frost, Tide, Iron, Dust, Stone, Vine, Salt, Arc, Jade, Spice)
**The D-READY-9 architecture (confirmed in workshop-outcomes.md) specifies:**
```rust
struct HeritageGrammarOverlay {
heritage_root: HeritageRoot,
boundary_character: BoundaryCharacter,
open_space_character: OpenSpaceCharacter,
structure_spacing: f32,
decorative_density: f32,
gathering_anchor_near_work: bool,
shared_infrastructure_preference: bool,
repair_visibility: f32,
facade_investment: f32,
signage_density: f32,
preferred_enclosure_tags: Vec<ObjectTag>,
accent_object_tags: Vec<ObjectTag>,
excluded_object_tags: Vec<ObjectTag>,
}
```
**What exists:**
- The struct specification in `miri-round4.md` — complete with values for all 10 roots
- The authoring domain allocation: Miri authors organizational principles; Araminta authors visual expression
**What does NOT exist:**
- Any `heritage_grammar_overlay/*.ron` (or equivalent) data files
- A `HeritageRoot` enum in `server/src/simulation/generator.rs` (the type is not defined; `SocietyProfileRef = String` is the only heritage-adjacent stub)
- The `ObjectTag` vocabulary that both heritage grammar and Araminta's asset categorization must share
- Any Phase 2 chunk fill code
**This is the largest cultural content gap in the pipeline.**
Every district's interior visual and spatial character — how property boundaries look, how open space is used, what objects appear, how dense and repaired the environment feels — flows from the `HeritageGrammarOverlay`. Without this content, Phase 2 chunk fill produces nothing differentiated. Every district looks the same regardless of whether it's a Frost-heritage frontier outpost or a Tide-heritage community hub.
**The heritage grammar data in `miri-round4.md` is complete and ready to author into files. The work is translation from workshop spec to authored data, not design.**
---
### Layer 8: Zone Palette — District Zones
**Status:** STUB — type defined as `type ZoneDefinition = String`
**Cultural inputs at this layer:** Zone identity per district type (LogisticsHub, Residential, Commercial, etc.) with heritage modifiers.
**What exists:**
- `server/content/global/zone-identity-spec.example.ron` — example zone spec (rural, well-structured)
- `server/content/global/van-maanens-star-rural-zone.ron` — Van Maanen's Star rural zone (exists)
- `server/content/global/van-maanens-star-industrial-zone.ron` — Van Maanen's Star industrial zone (exists)
**What is missing:**
- Zone definitions for the 9 DistrictType enum values: LogisticsHub, Residential, Commercial, Industrial, Administrative, Entertainment, MixedUse, Transit, Specialized
- Heritage modifier variants for each zone type
- The `ZoneDefinition` type needs to be promoted from `String` stub to a proper struct before zone content can be consumed
**Partial authoring exists for Van Maanen's Star only.** Other systems have no zone definitions.
---
### Layer 9: NPC Population
**Status:** STUB — `SocietyProfileRef = String`, NPC generation not implemented
**Cultural inputs at this layer:** Full culture profile (naming, voice, traits, behavior, heritage blend)
**What exists:**
- 3 culture RON files (Van Maanen's Star, Vael, Osse) — validated in voice pipeline Spike 1
- `wiki/authoring/cultural-generation-guide.md` — 5-dimension authoring framework
- `wiki/authoring/culture-authoring-guide.md` — RON file authoring process (Mellanie)
**What is missing:**
- ~227+ culture RON files (one per system, or per cultural corridor)
- Heritage root blend weights as an extension of the culture RON schema
- `wiki/cultural-groups/` directory is EMPTY — no canonical wiki entries for any culture
The `wiki/_templates/cultural-group.md` template exists and is well-structured. No cultural group entries have been authored using it.
---
## Summary Table
| Layer | Status | Cultural Data Status | Content Blocker? |
|---|---|---|---|
| Galactic (star/system) | DONE | Partial wiki coverage | No (numeric data in systems.db complete) |
| System → heritage mapping | — | MISSING | **YES — blocks Phase 1 social stage** |
| Planetary surface/heightmaps | DONE | N/A | No |
| Atlas markers / city naming | DONE | Naming operational; functional fields absent | Partial (need city marker schema extension) |
| Economics | DONE | Healthy | No |
| Phase 1 — Classification | STUB | No SettingType mapping rules authored | Yes |
| Phase 1 — Block grid | STUB | No heritage→layout probability authored | Yes |
| Phase 1 — Social site + NPC | STUB | 3/230+ culture files exist; no heritage blends | **YES** |
| Phase 2 — Heritage grammar overlay | MISSING | No authored data for any heritage root | **YES** |
| Phase 2 — Zone palettes | STUB | 2 Van Maanen's Star zones only | Yes |
| NPC culture profiles | STUB | 3 profiles, no wiki entries | Yes (can defer to first system) |
---
## Critical Content Gaps — Ordered by Blocking Impact
### Gap 1: System → heritage root blend (CRITICAL, blocks Phase 1)
No authored document establishes which heritage root blend applies to any system. The `SocietyProfileRef` stub cannot be resolved without this mapping.
**What's needed:** A mapping table (or per-system annotation) of `system_id → Vec<(HeritageRoot, f32)>`. This could live in:
- An extension of the culture RON file (add `heritage_blend` field)
- A new `heritage-map.ron` or similar global file
- Per-system wiki pages (in `wiki/star-systems/{sys}/index.md`)
The mapping for Van Maanen's Star can be derived from the existing content: the culture guide describes "working-class pragmatic" + "community-oriented" + "suspicious of authority" + "shows up and does the work." Cross-referencing with heritage root definitions from miri-round4.md, this reads as: **Iron** (collective labor identity, shared work) + **Stone** (tenure-based trust, territorial memory, slow disclosure) + **Tide** (communal eating, community participation, warm surface). A reasonable starting blend: Iron(0.4), Stone(0.35), Tide(0.25).
This is not a design question. It is translation of existing characterization into the generator's vocabulary.
### Gap 2: HeritageGrammarOverlay data files (CRITICAL, blocks Phase 2)
Ten heritage roots need authored overlay files. The full specification exists in `miri-round4.md` §OQ-R4-D. The values are already worked out for each root. What is needed is authoring them into a machine-readable format (RON or TOML).
**Scope:** This is Miri's content to author. Araminta then expresses the visual vocabulary against these overlays. The `ObjectTag` vocabulary must be agreed jointly.
**Blocking relationship:** Phase 2 chunk fill → HeritageGrammarOverlay data files → ObjectTag vocabulary → Araminta asset categorization.
### Gap 3: City marker schema extension (MODERATE, blocks Phase 1 input)
The markers.json city format (`id`, `name`, `kind`, `center`, `population`) is insufficient for Phase 1 district skeleton generation as described in ticket #899. Either city markers need `primary_function`, `planet_class`, `settlement_pattern` fields added, or Phase 1 must derive these from systems.db cross-reference.
**This is a Tyre/generator question as much as a worldbuilding question.** I flag it here because if new city marker fields are required, Miri is responsible for defining what `settlement_pattern` values mean culturally (grid-planned vs organic-pioneer), and Tyre is responsible for how `generate_atlas.py` populates them.
### Gap 4: Zone identity definitions for all DistrictType values (MODERATE)
The `DistrictType` enum has 9 values. Only 2 Van Maanen's Star zone files exist. Full zone coverage needs:
- One zone spec per DistrictType (base definitions)
- Heritage modifier variants (how does a LogisticsHub differ in Stone vs Tide vs Iron heritage?)
This is authoring work for Miri (social/organizational character) + Araminta (visual expression).
### Gap 5: Cultural group wiki entries (LOWER, documentation debt)
`wiki/cultural-groups/` is empty. The `wiki/_templates/cultural-group.md` template exists. Cultural data lives in authoring guides and RON files but has no canonical wiki presence.
For the generator, the RON files are what matter. For human authors (Mellanie, future content team), the wiki entries matter. This is documentation debt that should be addressed alongside Gap 1-3 but does not block code.
---
## Ticket Dependency Observations
### Existing tickets and their status
| Ticket | Title | Status | Notes |
|---|---|---|---|
| #615 | Tycoon small business owner starting state | backlog | Blocks #681 (apartment generator) |
| #616 | Broad economic verb vocabulary design | backlog | No pipeline dependency |
| #619 | Full character customisation | backlog | No pipeline dependency flagged |
| #679 | Location-to-culture resolution system | **DONE** | Culture name resolved by location — but heritage root blend not included |
| #681 | Apartment generator logic (server) | backlog | Blocked by #615 + #679 (done); needs culture |
| #682 | Apartment visual rendering + first-moment | backlog | Blocked by #681 |
| #694 | Character creation screen | backlog | Blocked by #685 (done) |
| #899 | District skeleton generator — Phase 1 | backlog | Blocked by #897 (this workshop) |
### Formal pipeline blockers (worldbuilding content view)
Character work (#694) and apartment work (#681, #682) sit downstream of:
```
Phase 1 district skeleton (#899)
← blocked by heritage root blend content (Gap 1)
← blocked by city marker schema (Gap 3)
← blocked by #897 (this workshop producing D-records)
Phase 2 chunk fill (no ticket yet)
← blocked by HeritageGrammarOverlay data files (Gap 2)
← blocked by Phase 1 (#899)
Apartment generator (#681)
← blocked by tycoon starting state (#615)
← culture resolution (#679, done) resolves culture name but NOT heritage root blend
Character creation (#694)
← no generation pipeline dependency; can proceed independently
← but the world they're dropping into won't be culturally differentiated until Gap 1-2 resolved
```
The apartment generator (#681) can produce a culturally-voiced apartment (NPC tone, naming, dialogue voice) using the existing `culture-van-maanens-star.ron`. What it CANNOT do is produce a spatially differentiated apartment that reflects heritage-root spatial grammar (boundary character, decorative density, facade investment) — that requires the HeritageGrammarOverlay (Gap 2).
This means #681 can proceed to a first version that is culturally voiced but spatially generic. The heritage spatial grammar layer can be added in a subsequent pass once HeritageGrammarOverlay data files exist. This is probably the right sequencing: don't block #681 further waiting for heritage overlays; instead, build the apartment generator against the culture RON files, and plan a follow-on ticket for heritage spatial refinement.
### New tickets needed (from worldbuilding perspective)
1. **Heritage root blend mapping** — Author `heritage-blend` field extension for culture RON schema + provide Van Maanen's Star blend. Prerequisite for Phase 1 meaningful district social assignment.
2. **HeritageGrammarOverlay data files** — Author all 10 overlay files from the `miri-round4.md` specification. Prerequisite for Phase 2 chunk fill.
3. **ObjectTag vocabulary** — Jointly author with Araminta. Prerequisite for heritage overlay files and asset categorization.
4. **Zone identity definitions** — Author canonical zone spec per DistrictType. Prerequisite for Phase 1 zone palette assignment.
5. **Van Maanen's Star cultural group wiki entry** — Formal wiki page consolidating all setting data. Documentation prerequisite; does not block generation.
6. **City marker schema extension** (coordination with Tyre) — Define and populate cultural/functional fields on city markers if Phase 1 requires them as inputs.
---
## What the Pipeline Promises and What's Missing
The generator-architecture workshop outputs describe a generator that produces culturally differentiated worlds — districts that feel different based on whether they're Stone-heritage frontier settlements or Tide-heritage community hubs, with visual, social, and spatial grammar that reflects the culture. That promise requires authored cultural data at two critical points:
**Point A — Phase 1 input:** Each district must know what culture it is generating for. This requires a system → heritage root blend mapping that doesn't exist yet.
**Point B — Phase 2 input:** The chunk fill that produces visible tiles must have HeritageGrammarOverlay data to apply the spatial grammar. This data doesn't exist yet.
The voice pipeline is healthy — three culture RON files are operational and the Van Maanen's Star profile is production-quality. The NPC voice layer will work. The spatial and visual differentiation layer will not, because it has no data.
The minimum viable cultural content set for a first walkable world output:
1. Heritage root blend for Van Maanen's Star → system (to test Phase 1 social assignment)
2. HeritageGrammarOverlay files for Iron + Stone + Tide (the three roots that make up Van Maanen's Star blend) → to test Phase 2 chunk fill differentiation
3. Zone identity specs for LogisticsHub + Residential (the two district types most relevant to Station Sova) → to test Phase 1 zone assignment
This is achievable in one sprint of content work. It is scope-limited to the primary test system, not all 230+ systems.
---
## Open Questions Raised by This Inventory
**Q1:** Is `SocietyProfileRef` intended to resolve to the culture RON filename (e.g., `"van-maanens-star"`), or to a more structured reference that includes heritage blend weights? The current String stub leaves this unresolved. If heritage weights are expected on the SocietyProfileRef rather than embedded in the culture RON, the culture RON schema needs a heritage_blend extension.
**Q2:** City marker schema — does Phase 1 read cultural/functional fields from markers.json city entries, or from systems.db body data? This changes both the atlas generator pipeline and the cultural data authoring requirements.
**Q3:** Are there 10 heritage roots or 7? The culture authoring guide (Mellanie) lists Frost / Tide / Iron / Dust / Stone / Vine / Salt (7). Miri Round 4 lists Frost / Stone / Tide / Iron / Dust / Vine / Salt / Arc / Jade / Spice (10). This needs resolution before HeritageGrammarOverlay files can be authored.
**Q4:** Should the `wiki/cultural-groups/` directory be populated now (one entry per culture RON file) or deferred until Phase 3 (planetary maps + station layouts)?
---
**Author:** Miri
**Date:** 2026-04-30
**Status:** Round 1 complete. Ready for Round 2 synthesis.
@@ -0,0 +1,251 @@
---
title: "Paula — Round 2: Narrative Structure Expressed Through Spatial Generation"
description: "Political and class legibility through district spatial parameters; naming registers; authored vs. seeded boundary"
type: workshop
status: active
workshop: generation-cascade
agent: paula
round: 2
created: 2026-04-30
---
# Generation Cascade — Round 2 (Paula)
**Focus:** What districts should express politically and socially through space alone; naming as narrative signal; where the authored/seeded boundary sits; minimum narrative floor for the thinnest vertical slice.
**Not addressed here:** NPC systems, heritage grammar overlays, culture files, social sites, triangle system. Those are outside scope.
---
## 1. What Districts Should Express Spatially
The honest truth is that political power and class have always been written into cities before anyone consciously designed them that way. The question is whether our generator makes those patterns legible to a player walking through a district without any UI overlay or NPC dialogue.
These are the spatial parameters that carry political information. They are all derivable from `DistrictType` + `WorldTier` + a prosperity scalar. No cultural content required.
### The Parameters
**`prosperity_index: f32` (0.0 → 1.0)**
This is the single most important signal and it doesn't exist in the current data model. Everything about class legibility flows from it:
| Prosperity band | Tile decay probability | Road width | Building density | Green space |
|-----------------|----------------------|------------|-----------------|-------------|
| 0.00.2 (subsistence) | 4060% tiles damaged/patched | Narrow | Very high lot coverage | None |
| 0.20.4 (working) | 2035% | Narrow to medium | High | Minimal (functional only) |
| 0.40.6 (middle) | 520% | Medium | Medium | Small plazas |
| 0.60.8 (affluent) | 15% | Medium to wide | Lower (setbacks) | Parks |
| 0.81.0 (elite/monumental) | <1% | Wide to boulevard | Low (wasted space is luxury) | Maintained gardens |
This is a new field proposal: add `prosperity_index: f32` to `DistrictSkeleton`. It's computed in Phase 1 Stage 1 from `WorldTier` + `DistrictType` + the city's economic profile in systems.db. No authoring required — it's derived.
**`perimeter_treatment: PerimeterTreatment`**
Currently the `access_points: Vec<AccessPoint>` field handles district entry/exit points. That's not sufficient. The *character* of those entry points is the political signal. Proposed enum:
```
Open // no boundary markers; permeable (organic/poor residential)
Fenced // visible boundary, no control (light industrial, transitional)
Walled // physical barrier, uncontrolled gaps (heavy industrial, abandoned)
Gated // checkpoints on main arteries, open on minor (elite residential)
Checkpoint // all entry points monitored (corporate security, administrative)
```
A player approaching a Checkpoint district understands before entering: this space is controlled by someone who has resources to control it. No NPC required. This field belongs on `DistrictSkeleton` alongside `access_points`. It's derived from `DistrictType` + `WorldTier` + `prosperity_index`.
**`layout_mode: DistrictLayoutMode`**
Already designed (Grid | Organic) and already in the spec. This is worth calling out explicitly as a political signal, because it is one:
- **Grid** districts were planned by someone with authority and resources. They signal deliberate power — administrative, corporate, institutional.
- **Organic** districts grew without central planning. They signal ungoverned space, or space that predates whoever is currently in charge. They feel older, stranger, harder to navigate.
A poor Organic district and a poor Grid district tell different political stories even at identical prosperity levels. One was always poor; the other was once planned and has since declined.
**Road width as status**
The workshop-outcomes.md spec notes `CorridorSpine` but doesn't specify widths as narrative signals. Proposal: road width should be a function of `DistrictType` + `prosperity_index` with deliberate political logic:
- Administrative districts: wide boulevards regardless of prosperity (power signals itself)
- Industrial: wide loading corridors on main arteries, narrow worker passages elsewhere
- Residential elite: wide main arteries, controlled secondary streets
- Residential working: uniform narrow (no hierarchy because no investment)
- Transit: widths are functional (vehicle-scaled), not status-scaled
The key signal here is **width hierarchy** — elite districts have a clear hierarchy from boulevard to secondary to alley. Poor districts have uniform narrowness. That uniformity reads as neglect.
**Sight lines as political geometry**
This one is harder to implement but worth flagging as a design principle for Phase 2 tile generation: sight lines carry political meaning.
- Can you see the administrative building from everywhere in the district? That's intentional design — presence asserted through visibility.
- A corporate security district has open sightlines through corridors (surveillance optimization).
- An organic working-class district has broken sightlines — buildings jut out, alleys angle away, nothing is legible at distance.
I'm not proposing full sight-line computation in the first pass. But Phase 2 tile selection should be aware that building placement along corridors in high-prosperity/Checkpoint districts should maintain sightlines, while Organic/low-prosperity districts should not. This is a parameter in the block fill algorithm, not a separate computation.
---
## 2. Naming With Narrative Weight
Every place name is a political act. Who gets commemorated? What function is elevated to a proper noun? What's left unnamed and called by folk description instead?
The generator needs naming *registers* — not just name pools. The register determines what *kind* of name is drawn. The culture pool provides the raw names.
### The Five Naming Registers
| Register | Pattern | What it signals | Typical districts |
|----------|---------|-----------------|------------------|
| **Commemorative** | Person's name, institutional name | Establishment power; official naming | Administrative, elite Residential |
| **Functional** | What happens here | Working class, transit-adjacent; named for use | Industrial, Transit, LogisticsHub |
| **Aspirational** | Abstract positive noun | Corporate development, gentrification | New commercial, MixedUse in growth |
| **Folk** | Colloquial, descriptive, sometimes ironic | Organic/ungoverned; named by inhabitants not officials | Old Residential, Backwater settlements |
| **Geographic** | Terrain feature, orientation | Pre-political naming; oldest settlements | Any SettingType with natural features |
**How the generator picks a register:**
This is a lookup table by `(DistrictType, WorldTier)` with weighted blends, not a hard assignment. Example:
| DistrictType | WorldTier | Primary register | Secondary register |
|-------------|-----------|-----------------|-------------------|
| Administrative | Epicenter | Commemorative | Aspirational |
| Administrative | Backwater | Folk | Functional |
| Industrial | any | Functional | Folk |
| Residential (high prosperity) | Regional+ | Geographic | Commemorative |
| Residential (low prosperity) | any | Folk | Functional |
| Transit | any | Functional | Numbered |
| MixedUse | Regional | Aspirational | Functional |
The `Backwater` WorldTier shift matters: an Administrative district in a Backwater system doesn't have the resources for commemorative naming. It gets folk names that happen to include the word "council" or "office."
This is worth a D-record. It's a locking decision about what the generator produces.
### Street Naming
Streets within a district should follow the district register but with additional signals:
- **Named streets** (proper noun) signal importance and age — this street was worth naming
- **Numbered streets** signal planned grid — authority imposed order
- **Descriptive streets** ("Canal Way", "Mill Road") signal function-first origin — working space
- **Colloquial streets** that don't match their character ("The Long Road" being short) signal living history — the name outlasted what it described
The generator can produce these mechanically: Grid layout districts use numbered secondaries. Organic layout districts use descriptive and colloquial names drawn from the culture folk pool.
### Landmark Naming
Landmarks inherit naming from the controlling faction, derivable from the economics layer. If ISC (a corporation in systems.db) operates in this system's logistics sector, the water distribution hub is "ISC Reservoir #4," not "Community Water Station." The economics layer already knows who controls what — landmark naming can pull from that directly, no authoring required.
---
## 3. The Seed-vs-Authored Boundary
Round 1 didn't address this directly. I want to be explicit about it because it determines authoring workload.
### What Should Be Authored Per City (Small Fixed Set)
**Political archetype — one field per city in atlas/systems.db:**
Five archetypes cover the vast majority of settlement types:
- `CompanyTown` — one corporation dominates; homogeneous district type distribution
- `AdminCapital` — government/institutional core surrounded by support districts
- `FreePort` — Transit and MixedUse dominant; contested/heterogeneous
- `Contested` — two or more factions with visible boundary zones
- `OrganicGrowth` — no dominant power; district types mix without hierarchy
This single field per city drives the district type *distribution* for Layer 6 (city → district decomposition). It's derivable from systems.db economics data for most cities, but someone needs to sanity-check the results for named cities in the setting. This is a small authoring task, not a large one.
**Named landmarks in major cities (authored by the team):**
Capital cities and named stations should have specific landmark names that anchor the setting. These are authored as part of Miri's work, not generated. The generator reserves the landmark slots; the authored names fill them for named cities.
### What Is Derived From Existing Data (Zero Additional Authoring)
- District type distribution → from `political_archetype` + systems.db economics data
- Prosperity index per district → from systems.db wealth indicators per city
- Naming register → from `(DistrictType, WorldTier)` lookup table (authoring the table once covers all cities)
- Landmark naming for unnamed/minor cities → from systems.db corporation data
- Perimeter treatment tier → from `DistrictType` + `WorldTier` + prosperity_index
### What Is Seeded (Deterministic Variation Per District)
- Block arrangement within district type + layout_mode constraints
- Specific tile decay placement within prosperity_index band
- Road width variation within allowed range per district type
- Perimeter treatment sub-tier (a working-class district might be Fenced or Open depending on seed, not authored)
- Street and landmark names for unnamed districts — drawn from culture pool, seeded per street
- Exact landmark footprint position within the reserved slot
**The key principle:** authored content is high-value and static across playthroughs (Van Maanen's Star's capital always has the same major landmarks). Seeded content produces variation within that stable frame. The generator never needs to invent political geography from nothing — it reads it from data that already exists.
---
## 4. Minimum Narrative Signal for the Thinnest Vertical Slice
If we generate one district with walkable tiles, what's the absolute minimum that makes it feel like a place with a story rather than a procedural grid?
Five signals. All derivable from existing or proposed architecture.
1. **`prosperity_index: f32`** — drives tile decay, road width, density. New field. One number. All class legibility downstream from this.
2. **`DistrictType`** — already in the struct. Determines the fundamental spatial vocabulary. Even with identical tile graphics, the *shape* of blocks differs between Industrial and Residential.
3. **`perimeter_treatment: PerimeterTreatment`** — new proposed field. The approach to a district is the first story beat. A player walking toward a Checkpoint district understands access control before entering. No tile variation needed — just the presence of the boundary marker tile at access points.
4. **`layout_mode: DistrictLayoutMode`** — already designed. Grid vs. Organic changes the political reading of any district. This one parameter tells the player whether this space was planned by power or grew without it.
5. **One active landmark slot** — already in the spec as `LandmarkSlot`. Even if it only manifests as a larger building footprint with no special behavior, it gives the district a focal point. A space with no orientation is spatially illegible — nothing says "something important happened here."
With these five signals, a player traversing a Checkpoint-perimeter, Organic-layout, low-prosperity Industrial district understands: this space has authority asserted over it from outside (checkpoint) but grew without planning (organic). It's economically marginal (decay tiles). Something notable anchors it (landmark footprint). That's narrative without dialogue, UI overlay, or NPC interaction.
**What this does NOT require for the first pass:**
- Heritage grammar overlays — district character is legible without them
- Culture-specific tile sets — decay vs. intact is sufficient; culture flavoring is Phase 2 enrichment
- Social site population — the physical district is readable before anyone lives in it
- Named streets — streets without names are still streets; naming enriches but doesn't unlock legibility
---
## 5. Positions on Round 1 Disagreements
**On #615 (Tycoon starting state) cascade placement:**
I agree with Gestalt. #615 is content design. The starting state description needs to know what *kinds* of spaces exist — the DistrictType taxonomy, the WorldTier values, what a prosperous district vs. a marginal one looks like in this setting. It does not need working generation code.
What #615 *does* need is OQ-R1-B resolved (SocietyProfileRef type) so it knows what cultural data the starting state can reference. Link #615 to that resolution, not to the generation pipeline milestone.
**On vertical slice effort (Gestalt 5-7 days vs. Tyre ~9 days):**
From a narrative perspective, stages 35 of Phase 1 (Reservations, Social sites, Guarantee audit) do not add to a player's ability to read a district as a place. They add simulation depth. The minimum for narrative legibility is Stages 12 (Classification + Block grid) + Phase 2 minimal fill.
That's the basis for Gestalt's lower estimate. I'd formally support deferring Stages 35 until after the first walkable district is demonstrably legible. Build the legibility floor first; add simulation depth once the basic spatial story works.
---
## 6. Proposed D-Record
**D-READY-P1: Spatial Class Legibility Requirements**
Districts must express political character through spatial parameters, not just type labels. The following fields are minimum requirements on `DistrictSkeleton` for a district to be narratively legible to a player traversing it without NPC interaction or UI overlay:
- `prosperity_index: f32` — new field; drives tile decay probability, road width tier, building density, green space allocation
- `perimeter_treatment: PerimeterTreatment` — new field; enum: Open | Fenced | Walled | Gated | Checkpoint; derived from DistrictType + WorldTier + prosperity_index
- `layout_mode: DistrictLayoutMode` — already in spec; must be respected by Phase 2 tile generation as a first-order spatial signal
Additionally, district and street **naming registers** (`Commemorative | Functional | Aspirational | Folk | Geographic`) must be resolved before Phase 2, as the naming of streets is part of the spatial output of the generator, not a post-generation overlay. The register is determined by `(DistrictType, WorldTier)` lookup; the names are drawn from culture pool.
**Reason to lock:** Phase 2 tile selection needs to know what signals to produce. If prosperity_index and perimeter_treatment aren't on DistrictSkeleton before Phase 2 is implemented, tile generation will produce spaces that are spatially indistinguishable regardless of district type — a simulation fidelity problem that's expensive to retrofit.
---
## 7. Open Questions From This Analysis
| ID | Question | Priority |
|----|----------|----------|
| OQ-R2-P1 | Should `prosperity_index` be explicitly authored for named cities (Van Maanen's Star capital) or always derived from systems.db economic data? The derived approach may produce counterintuitive results for scripted setting cities. | HIGH |
| OQ-R2-P2 | Is `political_archetype` a new field in atlas markers.json, or derived from existing systems.db data? If derived, what is the derivation rule? | HIGH |
| OQ-R2-P3 | Naming register lookup table: who authors the `(DistrictType, WorldTier)` → register mapping? This is a small task but needs ownership before Layer 6 generates district names. | MEDIUM |
---
*Paula — Round 2. Written 2026-04-30.*
@@ -0,0 +1,360 @@
---
title: "Paula — Round 3: Narrative Structure at Regional and City Scale"
description: "Political signals in regional maps, city orientation, road networks, settlement hierarchy, and naming strata — Phase 3 scope, above district level"
type: workshop
status: active
workshop: generation-cascade
agent: paula
round: 3
created: 2026-04-30
---
# Generation Cascade — Round 3 (Paula)
**Focus:** Phase 3 — planetary/regional maps, city footprints, roads, and settlement hierarchy. The narrative/political signals that should exist *above* district level, and the data the generator needs to produce them.
**Not revisited here:** `prosperity_index`, `perimeter_treatment`, `political_archetype`, naming registers — these were accepted in Round 2. I'm building above them.
**Not addressed here:** heritage grammar, NPC population, culture files, social sites. Out of scope.
---
## Framing the Scale
Round 2 was about a district being legible when a player walks through it. Round 3 is about what a player reads before they arrive — what the regional map, the city approach, and the road between settlements tells them about who controls what and why.
The political geography of a world is visible at altitude before it's visible on foot. Power writes itself into the landscape at every scale: which rivers are bridged (someone paid), which roads are wide (someone uses them heavily), which settlements have walls (someone threatened them), which settlements have *two* names (someone disputed them). The generator needs to produce those signals systematically, not accidentally.
These are not cosmetic. A player navigating a regional map is making decisions — where to go, what to expect — based on visible signals. If those signals are random noise, navigation is a guessing game. If they carry consistent political logic, the player reads the world.
---
## 1. Regional Maps: What Control Looks Like From Above
### The Core Signal: `TerritorialStatus`
Before any city-level decomposition, each region on a planetary map should carry a territorial status. This is the highest-level political signal — it determines what infrastructure exists and how the generator populates the space between cities.
Proposed enum:
```
CoreTerritory // firmly administered; full infrastructure
FrontierTerritory // administered in principle; infrastructure sparse
ExtractiveZone // controlled for a purpose; purpose-built infrastructure only
ContestZone // multiple claimants; infrastructure in conflict
WildernessBuffer // no administration; minimal infrastructure
AbandonedZone // was administered; currently not
```
**Derivation:** `TerritorialStatus` is not authored per region. It is derived from:
- Proximity to cities (distance to nearest city × that city's `WorldTier`)
- Systems.db economic data (does any corporation have an active presence here?)
- Whether the planetary body has a `political_archetype` that implies contested territory
Named planets in the setting should have their `TerritorialStatus` zones sanity-checked, not authored from scratch.
### What Each Status Signals
**`CoreTerritory`:**
Roads are maintained at the governing city's standard. Settlement density is higher. The wilderness is legible — trails exist, they go somewhere. There may be checkpoints at territory edges but not within the core. The player understands: someone is in charge here, and they've organized the space.
**`FrontierTerritory`:**
Road quality degrades sharply at the edge of administered zones. Settlements thin out and become more purpose-built (outposts, not towns). Infrastructure exists where the administrative authority cares about something (a resource site, a transit route) and not elsewhere. The player understands: administration exists but is stretched thin.
**`ExtractiveZone`:**
Corporate or administrative infrastructure is excellent along the extraction corridor and nowhere else. The road from the depot to the mine is better maintained than the road from the mine to any settlement. Settlements in extraction zones exist to support the extraction, not as independent communities. The player understands: this space belongs to whoever is extracting.
**`ContestZone`:**
The most visually distinctive status. Signs of multiple authorities: doubled checkpoints at boundaries, infrastructure that changes quality at invisible lines, sometimes two roads between the same points (each authority built their own). Settlement names may conflict (see §5 on dual naming). The player understands: this space is fought over, and the fighting is visible.
**`WildernessBuffer`:**
No infrastructure other than geographic trails (rivers, passes). Any settlements are isolated and self-reliant. The silence of the map here is the signal — nothing built here means nothing worth controlling here, or territory that resisted control. The player understands: if you go here, you're outside the protected system.
**`AbandonedZone`:**
The most narratively loaded status. Infrastructure exists but is degraded or dead — roads that lead to ruins, bridges maintained badly, relay stations dark. The question this status always raises: what was here, and why did it stop being valuable? The player understands: something changed. The world has a past that isn't entirely gone.
---
## 2. City Political Geography: Before the Districts
Round 2 gave us `political_archetype` to drive district type distribution. But there's a prior question: how does a city sit in its landscape, and what does that tell you before you've zoomed in far enough to see districts?
### `FoundingOrientation`
Every city was built around something. That something determines where the oldest district is, which direction the city grew, and where the decay gradient runs. Proposed enum for city-level (atlas/systems.db):
```
PortFacing // founded on maritime/waterway trade; oldest district faces water
RailHeadFacing // founded on transit convergence; oldest district faces the junction
ResourceFacing // founded to service extraction; oldest district faces the mine/farm/well
DefenseFacing // founded as fortification; oldest district faces a high point or natural barrier
AdminFacing // founded as administrative center; radiates from government building outward
```
**Why this matters for the generator:** District type decomposition already uses `settlement_pattern` from systems.db. `FoundingOrientation` adds a directional signal — the generator knows which end of the city is oldest, and therefore which end is most decayed, most organic in layout, and most working-class. The city has a political gradient from old to new, and the direction of that gradient is not arbitrary.
**Derivation:** `FoundingOrientation` is mostly derivable from `settlement_pattern` (port, inland, etc.) + `body.economic_role`. It needs a sanity pass on named cities where the setting lore implies a specific history.
### How `political_archetype` Shapes City Footprint
Round 2 established the five archetypes. Here I want to be concrete about what each archetype means for city *shape* before we decompose into districts — because that shape constrains the district layout.
**`CompanyTown`:**
The dominant corporate asset is not centered — it's at one end of the city (the resource-facing end or the port). Worker districts fill the space between the facility and the edge of nothing. There is no plaza, no civic center, no mixed-use heart. The city has a spine, not a center. The facility is the source; everything else is arranged in service of access to it. Transit infrastructure runs toward the facility, not outward toward other cities.
*Generator implication:* District decomposition places Industrial or Corporate Residential adjacent to the dominant facility end. Civic and Administrative districts are minimal and peripheral. City growth direction is away from the facility (workers pushed back from the asset).
**`AdminCapital`:**
The government complex is central, elevated if terrain permits, on the major artery intersection. Residential quality gradients radiate outward — the best residential is adjacent to the administrative center (proximity = status), the worst is at the urban fringe. Multiple well-maintained arteries converge on the administrative center (petitioners and officials both need access). The city has a center that asserts itself.
*Generator implication:* Administrative district occupies a central or elevated position. Road infrastructure is hub-and-spoke from that point. Residential quality (and by extension `prosperity_index`) decreases with distance from center.
**`FreePort`:**
No single center. Multiple competing centers of gravity — trading compounds, transit hubs, factional warehouses. The city is not organized around one institution but around the edges of many. Transit infrastructure is dense and well-maintained everywhere (commerce cannot tolerate bottlenecks). Prosperity and poverty intermix according to commercial logic, not spatial hierarchy: a rich trading house can sit adjacent to a destitute workers' barracks because that's where the docks are.
*Generator implication:* Multiple MixedUse and Transit districts. No dominant administrative core. Prosperity gradient is jagged, not smooth from center to edge. High `perimeter_treatment` variation between districts (some Gated compounds, some Open street markets, side by side).
**`Contested`:**
The most explicitly spatial archetype. There is a visible contested boundary inside the city — a street, a canal, a defunct rail line — that serves as a de facto border between two zones of control. Infrastructure quality changes abruptly at this line. On one side: high `perimeter_treatment`, Checkpoint or Gated districts. On the other: a different authority's version of the same. Dead space at the boundary: buildings that face both ways and belong to neither, or buildings that were destroyed and not rebuilt.
*Generator implication:* District decomposition places the highest `political_archetype` tension along a specific axis of the city, not randomly distributed. The contested boundary should be detectable as a feature before districts are even generated — it's a city-level geography question. Road infrastructure is duplicated or dead at the boundary axis.
**`OrganicGrowth`:**
No center. Multiple minor centers, none dominant. Infrastructure follows historical accident — the oldest road bent around a building that no longer exists; the street grid has four different orientations because four different periods of growth each imposed their own logic. Quality gradients are local, not city-wide: one neighborhood maintains itself, its adjacent neighborhood doesn't, and there's no political reason for the difference. The city resists legibility from above.
*Generator implication:* District decomposition produces high layout diversity (more Organic layout districts relative to other archetypes). Prosperity index varies locally rather than tracking a city-wide gradient. The `FoundingOrientation` is more ambiguous here — the city may have multiple founding nodes.
---
## 3. Road Networks as Political Artifacts
Roads are the most persistent political statements in a landscape. The question the generator needs to answer for every road is not just "does this road exist" but "who built it and why."
### `MaintenanceAuthority`
Proposed field for road segments:
```
Administrative // maintained by governing authority; quality proportional to distance from capital
Corporate // maintained by a corporation; uniform quality within corporate territory, abrupt drop at edge
Communal // maintained by settlements it connects; quality proportional to settlement prosperity
Trade // maintained by traffic volume; quality proportional to commerce flow, not political importance
Abandoned // was maintained; no longer is
```
**Why this matters:** Road quality tells the player who cares about a connection. An `Administrative` road from the capital is excellent near the capital and degrades at distance — that degradation is the visible expression of how far administrative power actually extends. A `Corporate` road has no gradient: it's uniformly good from facility to facility and then just... stops. The quality boundary is as sharp as a property line.
A `Trade` road through a politically marginal region can be excellent if commerce demands it — which is its own political signal. Commerce went where politics didn't bother to go.
### Road Narrative Classes (derived, not authored)
The generator should produce road narratives from existing data:
**Administrative corridor:** Connects administrative capital to sub-settlements or resource sites it administers. Quality inversely proportional to distance from capital. Checkpoints may appear at territorial boundaries (where `TerritorialStatus` changes). The capital end is always best-maintained.
**Corporate corridor:** Connects corporate assets (depot → mine → processing → port). Maintained at corporate standard regardless of location. The quality drop at the corporate boundary is intentional — the corporation maintains only what it needs. These roads often skip the nearest town entirely, because the corporation does not need the town.
**Trade route:** Quality proportional to traffic volume, not political importance. Can be excellent in areas that administrative authority barely reaches, if commerce demands it. Trade routes often follow `FreePort` settlement chains. Notable: trade routes can outlast the political conditions that created them — an old trade route connecting a now-contested or abandoned city still shows its quality level from the era of its use.
**Settlement track:** Maintained by the settlement(s) it connects. Quality follows settlement prosperity — a working-class settlement has a working-class road. These roads are often the oldest in a region, because settlements predated highways. They follow terrain more closely (less engineering capacity), which makes them more winding.
**Abandoned road:** Quality in physical decline — cracked surface, overgrown margins, unmaintained bridges. But the road still exists, still goes somewhere, and what it connects tells a story about a different era's values. An abandoned corporate road through a `WildernessBuffer` region asks: what was here that the corporation abandoned?
### Road Width as Compound Signal
Road width is already proposed in Round 2 as a function of `DistrictType` + `prosperity_index`. At the regional scale, it adds a third variable: `MaintenanceAuthority`.
- Administrative roads near the capital: wide, boulevard-standard, even through poor areas — power signals itself through infrastructure spending that exceeds necessity
- Corporate roads: wide enough for the heaviest vehicles the corporation operates, not wider
- Communal/settlement tracks: width proportional to traffic type, without excess
- Abandoned roads: whatever they were, now narrowed by encroachment
---
## 4. Settlement Hierarchy: What the Generator Knows About Non-City Settlements
The atlas shows cities. But cities don't exist in isolation — they exist in a landscape populated by smaller settlements that express their relationship to the nearest city. The generator needs to know what that relationship is.
### The Full Hierarchy
| Tier | Population | Generator treatment | Narrative function |
|------|------------|--------------------|--------------------|
| City | 10,000+ | Full district decomposition | Current power — who's in charge, what they built |
| Town | 1,00010,000 | Single-district equivalent | Economic dependency — what the city extracts and how it structures the relationship |
| Outpost | 501,000 | Purpose-typed single node | Strategic ambition — where power is trying to reach, or holding |
| Waypoint | <50 | Infrastructure node, no district | Control of movement — who manages the between-spaces |
| Rural structure | Isolated | Type-tagged single structure | Individual economic actors; legible only through their road connection quality |
| Ruin/Abandoned site | 0 | Historical footprint | What failed; what past valued that present no longer does |
### `PoliticalTether`
Every settlement below city-tier has a relationship to a city. This relationship determines road quality, economic dependency, and the settlement's vulnerability to change. Proposed field for sub-city settlements:
```rust
pub struct PoliticalTether {
pub tether_city: CityId,
pub relationship: TetherRelationship,
}
pub enum TetherRelationship {
Administered, // reports to city government; road maintained at city standard
EconomicallyDependent, // supplies city with goods; road maintained proportional to value of that supply
Contested, // claimed by this city and at least one other; double-maintained or abandoned roads
Independent, // de facto independent; locally maintained roads, no city investment
Corporate, // tethered to a corporation, not a city administration
Abandoned, // was tethered; tether city no longer exists or no longer functions
}
```
**Derivation:** `PoliticalTether` is derived from systems.db:
- Distance and road quality to nearest city → probable `tether_city`
- Systems.db economics data → which corporation operates in this zone → `Corporate` relationship
- City `political_archetype``CompanyTown` implies most sub-settlements are `EconomicallyDependent` or `Corporate`
**What `PoliticalTether` drives:**
- Road quality between settlement and its tether city
- Naming: `Administered` settlements may inherit naming register from tether city; `Independent` settlements generate folk names; `Abandoned` settlements retain their last naming layer frozen
- Settlement density in the region: `Administered` settlements cluster along administrative corridors; `Corporate` settlements cluster around corporate infrastructure; `Independent` settlements scatter without pattern
- Ruin likelihood: when a `CompanyTown` tether city's dominant corporation leaves (derivable from economic data), its `EconomicallyDependent` and `Corporate` sub-settlements are ruin candidates
### Ruins as Narrative Infrastructure
Abandoned sites deserve special treatment because they are the most politically loaded settlement type. A ruin is not an absence — it is a presence with an explanation gap.
The generator knows:
- What the site was (previous `TetherRelationship`, previous settlement type)
- Who abandoned it (economic change? corporate departure? military destruction?)
- What road connects to it (road quality before and after abandonment era)
The player sees:
- A site marked differently on the map (different marker type)
- A road that leads to it — quality decayed but still there
- No administrative infrastructure, but physical footprint
The gap between what the generator knows and what the player can read is the gameplay. The generator does not need to present the explanation. It needs to produce the spatial evidence and let the player interpret it.
---
## 5. Naming at Regional Scale: Political History as Strata
District-level naming registers (from Round 2) apply to spaces that a single authority controls now. Regional-scale naming is different: it carries sediment from every authority that ever claimed the region, because physical features outlast political systems.
### How Naming Strata Work
Regional names accumulate in layers, with newer layers sometimes obscuring and sometimes coexisting with older ones:
**Layer 1 — Geographic (oldest):** The terrain feature gets named for what it is or where it is. Rivers, mountain ranges, plains — described in terms anyone on the ground could verify. This layer predates all recorded authorities and often predates settlement. It survives even through complete political turnover because travelers still need to refer to the mountain, and "the mountain" is still there.
**Layer 2 — Claiming (authority-imposed):** When an authority claims a region, they rename it. They impose commemorative names on previously-geographic features (the river becomes the "Founder's River") and assign administrative names to new territorial divisions. This layer signals historical dominance — if the geographic Layer 1 name has been fully replaced, the claiming authority has been in place long enough to normalize its naming.
**Layer 3 — Functional (working):** Roads, transit corridors, and industrial regions acquire functional names through use. "The Northern Highway." "Iron Corridor." These are rarely official origin — they emerge from workers, traders, and travelers who need to refer to a route and reach for what's obvious. Functional names persist because they're practical, not because they're endorsed.
**Layer 4 — Contested (dual naming):** A region with two names in active use — one official, one folk — signals that the previous authority's community still exists and uses the old name. This is the most politically charged naming state. The gap between `official_name` and `common_name` is a form of visible resistance.
### `DualNaming` Field
For politically significant regions, roads, and major geographic features, the generator should support two name fields:
```rust
pub struct RegionalFeatureName {
pub official_name: String, // current authority's designation
pub common_name: Option<String>, // folk/historical name if different
}
```
`common_name` is populated when:
- `TerritorialStatus` is `ContestZone` or `AbandonedZone` (high residual naming conflict)
- The region changed hands within a derivable historical period (from systems.db political history)
- The region's `political_archetype` is `Contested` (dual names at city level too)
When `official_name == common_name` (or `common_name` is None): stable, settled territory. No active naming conflict.
When they differ: the player is in disputed or transitional territory. The naming conflict is a gameplay signal without a tutorial prompt.
### Regional Naming Patterns by Feature Type
**Rivers:** Geographic register primary. If a river is a political boundary (derivable from being on the edge of two `TerritorialStatus` zones), it may acquire an administrative name for that boundary function. Folk names for politically-boundary rivers tend toward the ironic or literal: "The Boundary," "The Divide," "No Man's Creek."
**Mountain ranges:** Geographic register. Exception: if a range is a resource site (mining, energy extraction), it acquires functional names for the working zones within it. The range as a whole keeps its geographic name; the specific peaks and passes get functional names if they're economically active.
**Plains, basins, valleys:** The most politically contested landscape type — this is where populations live and food grows. These regions carry all four naming layers most visibly. A valley may have a geographic name (the original description), a claiming name (whoever conquered it and built the administrative hub), a functional name (what it produces), and a common name (what the people who live in it call it, which may be the pre-conquest name). For generation: these regions should almost always have `DualNaming` when `TerritorialStatus` is `CoreTerritory` or `ContestZone` and the region has a population above a threshold.
**Administrative regions/provinces:** Almost always `Commemorative` naming at founding. The most politically volatile names — new authorities rename provinces as a political act. For generation: administrative region names should be `Commemorative` register, with `common_name` carrying the pre-renaming folk/geographic form when the change is recent enough to generate resistance.
**Roads:** Follow the `MaintenanceAuthority` and naming register rules:
- `Administrative` roads: `Commemorative` register for named highways (named after political figures); numbered for secondary roads (authority imposed a number system)
- `Corporate` roads: corporation name as prefix; functional or numbered suffix
- `Communal`/`Trade` roads: `Functional` register; sometimes `Folk` for the most-used routes
- `Abandoned` roads: retain their last naming layer, frozen at the time of abandonment — which is itself a temporal signal to the observant player
---
## 6. Connecting Regional Signals to District Generation
The fields proposed here propagate downward. This is how Phase 3 inputs drive Phase 5 (district generation):
```
TerritorialStatus
→ affects road quality standards the generator applies
→ affects settlement placement density
→ informs naming layer conflict (DualNaming)
FoundingOrientation (city-level)
→ determines which direction is "oldest" for city decomposition
→ district prosperity index gradient runs from founding orientation outward
→ oldest districts more likely Organic layout, lower prosperity
PoliticalTether (sub-city)
→ determines road maintenance class for connections
→ informs whether sub-city settlement generates ruins or remains active
→ informs naming register of sub-city settlement name
DualNaming (regional/road/city)
→ propagates to district names when TerritorialStatus = ContestZone
→ district naming register shifts toward Folk/Geographic when the city's official
naming is in tension with common use
```
The key principle: **regional political signals are inputs to district-level generation, not independent outputs.** If a city is in a `ContestZone`, its districts should reflect that — higher `perimeter_treatment` variance, more contested naming, higher likelihood of Organic layout even in theoretically-planned districts. The generator doesn't need to be told twice. It reads from the regional data and produces district data that is consistent with it.
---
## 7. Proposed New Fields Summary
| Field | Applies to | Type | Derivation | Authoring needed? |
|-------|-----------|------|------------|-------------------|
| `TerritorialStatus` | Region/zone | Enum (6 values) | Distance to city × WorldTier + econ data | Sanity check for named planets |
| `FoundingOrientation` | City | Enum (5 values) | `settlement_pattern` + `economic_role` | Sanity check for named cities |
| `MaintenanceAuthority` | Road segment | Enum (5 values) | Road endpoint types + econ data | None |
| `PoliticalTether` | Sub-city settlement | Struct (city_id + relationship) | Distance + econ data | None |
| `RegionalFeatureName.official_name` | Region/road/feature | String | Generator | Named features authored |
| `RegionalFeatureName.common_name` | Region/road/feature | Option\<String\> | From historical naming conflict conditions | None for generated; authored for named |
All six are derivable from data that already exists or is being proposed. None require large authoring tasks. Named planets/cities need a sanity pass.
---
## 8. Open Questions for Round 3
| ID | Question | Priority |
|----|----------|----------|
| OQ-R3-P1 | `TerritorialStatus` derivation: is the formula `distance to nearest city × WorldTier` sufficient, or does it require additional parameters from systems.db? Does Burnelli-Sheldon have economic inputs that should feed this? | HIGH |
| OQ-R3-P2 | `FoundingOrientation` and `PoliticalTether` — are these stored in atlas/markers.json, or derived at runtime from systems.db? If stored: who authors the sanity-check pass for named cities, and what does that pass require? | HIGH |
| OQ-R3-P3 | `DualNaming`: is `common_name` a generator output (seeded based on territorial conflict conditions), or an authored layer only for named locations? If generator output, what triggers its population? | MEDIUM |
| OQ-R3-P4 | Road segments are currently implicit (markers.json has road topology but not road-segment-level fields). Does adding `MaintenanceAuthority` require a schema change to markers.json road data? Tyre's input needed. | MEDIUM |
| OQ-R3-P5 | `AbandonedZone` settlements as ruins: the generator knows what the site was (from historical tether data), but does it know *why* it was abandoned? Is that derivable from economic data (corporation left → corporate-tethered settlements abandoned), or does it require a separate authored flag? | LOW |
| OQ-R3-P6 | Phase 3 data storage: do `TerritorialStatus` zones get stored as polygon/voronoi regions in a new table, or as tags on city markers? A region between cities is not currently a first-class object in the data model. | HIGH |
---
## 9. Cross-Referencing Accepted Round 2 Proposals
For completeness: the six fields above are *additions* to the Round 2 locked proposals. The relationship is:
- `prosperity_index` (Round 2) receives its city-level baseline from the city's position within its `TerritorialStatus` zone. A city in `ExtractiveZone` has different prosperity baseline distribution than one in `CoreTerritory`.
- `perimeter_treatment` (Round 2) at district level is informed by the city-level `PoliticalTether` relationships — a city surrounded by `Corporate`-tethered sub-settlements has a different perimeter treatment profile than one surrounded by `Administered` towns.
- `political_archetype` (Round 2) is now grounded in city `FoundingOrientation` — the archetype shapes district distribution, and `FoundingOrientation` shapes the spatial direction that distribution runs.
- Naming registers (Round 2) are now the bottom tier of a three-tier naming system: regional naming strata (§5) → city-level `political_archetype` naming → district-level naming register.
---
*Paula — Round 3. Written 2026-04-30.*
@@ -0,0 +1,308 @@
---
title: "Paula — Round 4: Narrative Requirements for the Planet-Down Cascade"
description: "Narrative/political signals at each cascade layer; TerritorialStatus as Layer 2 output; accepted proposals remapped; determinism rule as narrative feature"
type: workshop
status: active
workshop: generation-cascade
agent: paula
round: 4
created: 2026-04-30
---
# Generation Cascade — Round 4 (Paula)
**Focus:** Domain requirements for the next workshop brief. Four cascade layers (planet-down), narrative/political lens. What each layer must produce for the world to be politically legible. Where accepted proposals sit in the new ordering. What the determinism rule means for narrative.
**Not revisited:** prosperity_index, perimeter_treatment, political_archetype, naming registers, sub-settlement hierarchy, MaintenanceAuthority — all carried forward. This document maps them to the new layer sequence.
---
## The Reframe: Geography Is Political History
The planet-down cascade gets the causality right. Cities don't create geography. Geography creates cities — and then political structures grow to control what geography made valuable.
The narrative consequence is significant. In the old city-outward framing, we had to work backwards from settlement position to infer what geographic logic placed it there. In the planet-down framing, the geographic logic comes first. We don't have to infer why a city exists at a river confluence — the river confluence was tagged at Layer 1, the settlement was placed at Layer 2 because of that tag, and the settlement's character is immediately legible from its founding condition.
This is a better narrative model because it matches how civilizations actually work. The political power of any settlement derives from what geographic advantage it controls. A city at a mountain pass doesn't just happen to have checkpoint-heavy districts — it was placed there because controlling the pass was the point. The generator doesn't need to be told that a pass city is administratively oriented; the geography tells it.
The four layers below address what each layer must produce for this to work.
---
## Layer 1: Empty World — Tagging Geographic Potential
**What the layer does:** Heightmap → water simulation (rivers, lakes, coastlines) → sub-biome refinement.
**Narrative position:** This layer produces no political signals — civilization hasn't arrived yet. What it produces is **geographic potential**: the affordances that will determine where civilization concentrates and what political structures that concentration supports.
### What Layer 1 must tag for Layer 2 to work
The settlement placement algorithm at Layer 2 cannot derive settlement character from raw heightmap data efficiently. Layer 1 should produce a tagged feature layer alongside the terrain output — geographic features that will become politically meaningful once inhabited.
**Mandatory tags for narrative-legible settlement generation:**
| Tag | What it marks | Political potential |
|-----|--------------|---------------------|
| `RiverConfluence` | Where two navigable rivers meet | Trading nexus; FreePort or AdminCapital archetype |
| `CoastalHarbor` | Natural harbor, protected anchorage | Maritime commerce; FreePort archetype |
| `MountainPass` | Lowest crossing point in a range | Checkpoint/control; AdminCapital or Contested |
| `ArablePlain` | Flat land with soil quality above threshold | Agricultural settlement; dependent on nearest nexus |
| `ResourceConcentration` | Mineral outcrop, energy source, rare material | Extraction; CompanyTown archetype |
| `Defensible` | Elevated with sightlines, natural chokepoint | Military/administrative; AdminCapital |
| `NaturalBarrier` | Range, wide river, dense terrain — impassable without engineering | Future political boundary |
These tags are the layer's narrative output. They're the geographic skeleton onto which all political structure will attach.
### What Layer 1 does not produce
Layer 1 does not name anything. Rivers, mountains, and passes exist at Layer 1 but have no names. Names are attached at Layer 2 when settlement begins. This is the correct order — geographic names are the oldest naming layer precisely because the terrain existed before anyone arrived to name it. The generator should preserve this layering: names are first assigned at the point when the settlement placement algorithm acknowledges a feature as settleable or traversable.
The one exception: major terrain features that are large enough to be visible on the planetary atlas (significant mountain ranges, major river systems, ocean boundaries) should receive a geographic placeholder name at Layer 1 so that the atlas UI can label them. These are geographic register names — purely descriptive, pre-political.
---
## Layer 2: Population Overlay — Civilization's Shape Emerges
**What the layer does:** Economics onto geography. Settlements placed at geographic nexus points. Forests felled for farmland. Hills flattened for towns. Road and rail networks connecting settlements.
**Narrative position:** This is the most politically loaded layer. Here the relationship between geography and power is established. Every decision the placement algorithm makes — where to put a settlement, what kind of settlement, how to connect it — produces a political fact that will still be legible at street level.
### Geographic rationale → settlement character
In the planet-down cascade, settlement character is directly derivable from the geographic tag that caused the settlement to exist:
| Geographic trigger | Settlement character | Political archetype lean |
|-------------------|---------------------|--------------------------|
| `RiverConfluence` | Trading city; port infrastructure | FreePort or AdminCapital |
| `CoastalHarbor` | Maritime hub | FreePort |
| `MountainPass` | Checkpoint city; controls access | AdminCapital; Contested if two powers compete |
| `ArablePlain` | Agricultural town or village cluster | EconomicallyDependent on nearest nexus |
| `ResourceConcentration` | Corporate/extraction town | CompanyTown |
| `Defensible` | Administrative or military center | AdminCapital |
| No geographic trigger | Settlement exists because someone decided it should | OrganicGrowth — the most politically ambiguous case |
The last case is the most interesting narratively. A settlement with no geographic rationale — placed in flat, resource-poor, non-nexus territory — exists because of a political decision that overrode geographic logic. Imperial projects, religious sites, symbolic capitals placed away from natural advantages. These are always the most politically contested settlements in real history, and they should be in the generator too.
### TerritorialStatus as Layer 2 output
In Round 3 I proposed TerritorialStatus as a regional control field. The lead flagged this as "wrong axis" when thinking city-outward. In the planet-down cascade, TerritorialStatus is correct — but as an **output of Layer 2**, not an input.
Once settlement placement and road network generation are complete, the space between settlements has a territorial character that falls directly out of:
- Settlement density (how many settlements per area unit)
- Road quality connecting those settlements (who invested in connection)
- Economic control data (which corporations are active)
- Distance and WorldTier of the nearest major settlement
The six TerritorialStatus values aren't assigned before Layer 2 runs — they're DERIVED after it completes:
- **`CoreTerritory`:** High settlement density + road network well-maintained throughout. Someone has built and maintained the infrastructure between settlements. Power is present and exercised.
- **`FrontierTerritory`:** Settlement placed, road connecting it exists, but road quality degrades at distance from the major city. Administration exists in principle; infrastructure thins at the edges.
- **`ExtractiveZone`:** Settlement placed at `ResourceConcentration`. Corporate presence flagged in systems.db. Road network excellent along the extraction corridor; absent or poor off it. The infrastructure tells you exactly what the controlling authority cares about.
- **`ContestZone`:** Two settlement clusters with competing road networks in overlapping territory. Geographic trigger is present but two political actors responded to it. Roads sometimes duplicated, sometimes dead at contested boundaries.
- **`WildernessBuffer`:** No settlement placed — the placement algorithm found no geographic trigger or economics rationale. Road does not cross. Not absence of interest; absence of occasion.
- **`AbandonedZone`:** Settlement was placed (geographic rationale existed at generation time), but economic simulation has flagged the rationale as exhausted. Resource depleted, corporation withdrawn, route obsoleted. The settlement's physical footprint remains; its economic basis has gone.
This resolves the ambiguity from Round 3's OQ-R3-1 (TerritorialStatus scope debate). TerritorialStatus doesn't need its own dedicated layer — it is a **post-processing output of the settlement placement and road generation steps** that already exist in Layer 2. It costs one classification pass after settlement placement completes, not a separate layer.
### Road networks: the infrastructure of political will
Roads in the planet-down cascade are not authored — they emerge from the connections the placement algorithm makes. But which settlements get connected at what quality reflects political priorities:
- The placement algorithm connects settlements based on economic flow (systems.db trade data) and administrative hierarchy (systems.db `economic_role`)
- Road quality is set at generation time based on the connection's economic value and the WorldTier of the endpoints
- `MaintenanceAuthority` falls directly out of the endpoint relationship:
- Corporate extraction site → corporate depot: `Corporate`
- Administrative capital → administered sub-settlement: `Administrative`
- Settlement cluster with trade flow but no administrative relationship: `Trade`
- Settlement → settlement with no economic tie and no administrative relationship: `Communal`
- Connection that existed at generation time but whose rationale has since dissolved: `Abandoned`
`MaintenanceAuthority` is derivable at road generation time. It does not require a separate authored pass.
### Sub-settlements: geographic inevitabilities
Sub-settlements (outposts, waypoints, rural nodes) emerge from Layer 2 geography, not from proximity to cities. The geographic logic makes them inevitable:
- A mountain pass will have a waypoint — passes always acquire service infrastructure
- A long road without settlements will have relay stations — travel demands it
- A river crossing will attract a settlement — crossings are traffic bottlenecks, and traffic bottlenecks attract commerce
- An agricultural cluster below city-threshold population produces a rural node — food production always aggregates to a spatial locus
`PoliticalTether` is then derivable from why the sub-settlement exists: what geographic feature caused its placement, and which city controls that feature. A waypoint at a pass controlled by an AdminCapital city is `Administered`. A waypoint at a pass that two cities both claim is `Contested`. A mining camp at a corporate extraction site is `Corporate`. The relationship emerges from the geographic trigger and the political control data — it doesn't need to be authored.
### Naming at Layer 2
Settlement names are first assigned here. The naming register for a settlement derives from its geographic trigger:
- `RiverConfluence` → Geographic register (the confluence is named before the city)
- `ResourceConcentration` + corporate presence → Functional register (the resource is the name)
- `AdminCapital` archetype → Commemorative register (the founder gets to name the capital)
- `ArablePlain` agricultural cluster → Geographic or Folk register (farmers named for what they see)
DualNaming conditions are set at Layer 2 wherever `ContestZone` territory exists — the naming conflict is a consequence of the territorial conflict, and both emerge from the same placement logic.
Major geographic features (rivers, mountain ranges) that were given placeholder names at Layer 1 receive their first human-assigned names here, at the moment settlements begin to use them. The Layer 1 geographic placeholder may persist as the common name while the Layer 2 administrative name becomes official — this is the origin of most DualNaming conditions.
---
## Layer 3: City-Level Planning — Political Archetype Becomes Spatial
**What the layer does:** Districts, suburbs, industrial zones, commercial areas, ports, stations, highways. Topography + economics + cultural parameters.
**Narrative position:** This is where the geographic rationale from Layer 2 becomes visible in city structure. The city knows what it was built for (FoundingOrientation, derived from the geographic trigger that placed it). That purpose shapes every spatial decision.
### FoundingOrientation is geography, not authoring
In the planet-down cascade, `FoundingOrientation` is not a derived field requiring a sanity-check pass on named cities. It is **directly readable from the geographic trigger**:
| Layer 2 trigger | FoundingOrientation |
|-----------------|---------------------|
| `CoastalHarbor` or `RiverConfluence` with port | `PortFacing` |
| Transit/rail junction | `RailHeadFacing` |
| `ResourceConcentration` | `ResourceFacing` |
| `Defensible` terrain | `DefenseFacing` |
| No geographic trigger (AdminCapital placed by political decision) | `AdminFacing` |
The generator reads the geographic tag that caused this settlement to exist and produces the appropriate orientation. No authoring, no sanity pass except for named cities where the setting lore explicitly overrides the geographic logic.
### How political_archetype shapes district layout direction
The combination of `FoundingOrientation` + `political_archetype` determines which direction the city grows and which end is oldest:
- `PortFacing` + `FreePort`: City grew away from the harbor (older densely packed districts near the water; newer more spacious development inland). The harbor is the source; the rest of the city is infrastructure for handling what the harbor produces.
- `ResourceFacing` + `CompanyTown`: City is a spine, not a circle. The resource is at one end; everything else is arranged in service of access to it. There's no civic center — the city has no need of one.
- `DefenseFacing` + `AdminCapital`: City radiates from the fortified high point. Administrative districts are adjacent to the fortification; residential quality grades outward from that center.
- No geographic trigger + `OrganicGrowth`: City grew around multiple small centers of gravity. No single direction is "oldest." The prosperity gradient is local, not city-wide.
The growth direction tells the generator which end of the city gets older, more organic, lower-prosperity districts, and which end gets the planned, newer, higher-prosperity development. This is the district prosperity gradient — it runs from founding end toward expansion end, with the oldest districts closest to whatever the city was built around.
### Naming at Layer 3
Districts receive names at Layer 3. The naming register follows the lookup table (Commemorative / Functional / Aspirational / Folk / Geographic by DistrictType + WorldTier).
The city's `political_archetype` adds a modifier:
- `CompanyTown` cities: district names lean Functional or Corporate (the company names things for what they do)
- `AdminCapital` cities: district names lean Commemorative (administrative power names things for its history)
- `FreePort` cities: district names lean Functional or Aspirational (commerce names things for what they offer)
- `Contested` cities: district naming is where DualNaming most visibly proliferates — each faction named the districts they control; the contested middle zone may have three names
---
## Layer 4: Street-Level Rendering — Political History Made Visible
**What the layer does:** Streets, buildings, civilization tiles.
**Narrative position:** This is where all upstream context is consumed to produce what the player actually sees. The narrative signals that have been building since Layer 1 become tactile — visible in tile state, building condition, perimeter markers, street width, landmark footprints.
### What the player reads at street level
From Layer 1: The city exists here because a river meets a coast, or because the pass is here. Even at street level, you can see the geographic rationale — the harbor is at one end of the main boulevard, the mountain wall is at the other end of the pass city.
From Layer 2: The territorial character of the region is visible in road quality at the city's approach. An `ExtractiveZone` city has excellent road infrastructure on the corporate corridor and nothing on the secondary routes. A `ContestZone` city has checkpoint infrastructure that doesn't fully match any one political actor's logic — overlapping, redundant, or dead.
From Layer 3: The district layout tells you what the city was planned to be. The grid on the administrative side is deliberate. The organic tangle near the old harbor is what grew before anyone planned it. The street hierarchy (boulevard → secondary → alley) is only present in districts that had investment — where it's absent, you're in a district that the planning authority didn't think was worth a hierarchy.
From Layer 4: The current economic state shows in tile condition. Prosperity_index drives decay probability. Perimeter treatment tiles at district edges show who controls entry. Building condition reflects current economic health, not the intended plan.
The narrative emerges from all four layers simultaneously. The player reads: intended geography → what was planned → what the sim currently says → what they see on the ground.
---
## The Determinism Rule: Narrative Consequences
The lead's critical rule: **economic sim affects rendering (prosperity/repair), NOT layout (streets locked by seed)**.
This is the most powerful narrative mechanism in the cascade. Let me be explicit about what it means.
### The ghost city effect
Streets are locked at generation time. They carry the intended political geography of the district — the street grid that was planned when the city was laid out. The economic simulation can change tile state, building condition, and prosperity_index (or a prosperity_delta applied to the seed-derived baseline), but it cannot move streets or erase landmark footprints.
The consequence: **a district's current state and its intended state are always both visible**.
A CompanyTown where the corporation has departed still has its company-town street vocabulary: wide factory roads built for heavy vehicles, worker housing in tight grids sized for maximum occupancy, no civic center because the company never built one. The streets broadcast what this district was planned to be. The tiles broadcast that no one is maintaining that plan anymore.
A once-affluent residential district that has declined still has its wide boulevards and generous setbacks — the spatial signature of wealth — even as tile decay tells you that the resources to maintain that plan have left. The player reads the gap: someone planned this for people with more than they needed. They're gone.
A Contested city that resolves in favor of one faction still has both factions' street-scale infrastructure. The streets the losing faction built persist. Their checkpoints are unmanned or controlled by the other faction, but their footprints remain. History is visible in structure even after political change.
### Prosperity_delta, not prosperity replacement
For this to work correctly, the economic sim must communicate with the tile generator through a **delta**, not a replacement value:
- **Seed-derived `prosperity_index`:** What the district was planned as. Set at Layer 3 from WorldTier + economic data. Locked.
- **Sim-derived `prosperity_delta`:** What the current economic state says about this district relative to what it was planned as. Can be positive or negative. Updated by the economic simulation.
- **Effective prosperity:** `prosperity_index + prosperity_delta` (clamped to [0.0, 1.0]) drives tile selection.
The seed-derived prosperity_index is what the street geometry was planned for. The effective prosperity is what the tiles show. When they differ significantly, the player is looking at the ghost city effect — the story of what happened between the plan and now.
This is not a technical refinement; it's a narrative requirement. If prosperity_index is just a sim-updated value with no seed-derived baseline, the ghost city effect disappears. The streets would then be random relative to the tile state, and the most powerful narrative signal in the game would be lost.
### Ruins as structural permanence
The determinism rule also applies to ruins (AbandonedZone sub-settlements). When the economic sim flags a settlement for ruination — resource depleted, corporation departed, route obsoleted — the settlement's physical structure remains. The streets are still there. The building footprints are still there. What changes is:
- Prosperity_delta drops sharply (or drops all the way to floor)
- Perimeter treatment degrades (Checkpoint → Fenced → Open over time, as maintenance ends)
- Landmark condition deteriorates
The ruin is a ghost city at settlement scale. The geography tagged it as worth settling. Someone built here. Now no one maintains it. The gap between the structure (still present) and the tile state (decayed) is the entire story of what happened, without a word of dialogue or UI explanation.
---
## Accepted Proposals: Remapped to the Planet-Down Cascade
| Proposal | Origin | New cascade position |
|----------|--------|---------------------|
| `prosperity_index: f32` on DistrictSkeleton | Round 2 | **Layer 3 output** (seed-derived baseline from WorldTier + geographic context); **economic sim** produces `prosperity_delta` at runtime |
| `perimeter_treatment: PerimeterTreatment` | Round 2 | **Layer 3 output** (derived from DistrictType + WorldTier + prosperity_index at generation time); **economic sim** can degrade it at runtime |
| `political_archetype` (derived + override) | Round 2 | **Layer 2 output** (derived from geographic trigger + economic data; override slot for named cities) |
| Naming registers (lookup table) | Round 2 | **Layers 24**: Geographic register at Layer 2, Functional/Commemorative at Layer 3, street-level naming at Layer 4 |
| Sub-settlement hierarchy | Round 3 | **Layer 2 output** (settlements emerge from geographic triggers; PoliticalTether derived from why they exist) |
| `MaintenanceAuthority` on roads | Round 3 | **Layer 2 output** (derived from road endpoint relationships; no separate pass required) |
| `TerritorialStatus` | Round 3 | **Layer 2 post-processing output** (derived from settlement density + road quality + economic control after placement completes; not a separate layer) |
| `FoundingOrientation` | Round 3 | **Layer 2 → Layer 3 handoff field** (directly readable from the geographic trigger that placed the settlement; no authoring required) |
| `DualNaming` (`official_name`/`common_name`) | Round 3 | **Layer 2 initialization** (ContestZone conditions set here); **Layer 3 proliferation** (factions name districts differently); **Layer 4 display** (streets have working names vs. official names) |
---
## Narrative Questions That Need Answering at Each Layer
These are questions the brief should surface, not questions I'm answering here.
**Layer 1:**
- Does the empty world generator produce a tagged feature layer alongside terrain output, or does Layer 2 have to re-derive geographic potential from raw heightmap data? The tagged layer approach is cleaner but requires a Layer 1 schema change.
- How are major geographic features (significant river systems, mountain ranges) identified for atlas-visible labeling vs. local features not worth labeling at atlas scale?
**Layer 2:**
- What triggers an OrganicGrowth settlement (no geographic trigger) vs. a settlement the algorithm decides not to place? Algorithmically they're both "no trigger found" — but narratively they're different (one is a settlement that exists anyway; the other is empty territory). Is there a way to distinguish them in the data model?
- How does the economic sim know when to flag a settlement for AbandonedZone status? Is this a threshold on economic flow below a minimum, or does it require an explicit event (corporation withdraws)?
- Sub-settlements that exist because of geographic inevitabilities (mountain pass waypoints, river crossing settlements) — do they get placed by the same algorithm as city settlements, or do they need a separate sub-settlement placement pass?
**Layer 3:**
- The prosperity gradient runs from founding end toward expansion end. How steep is the gradient? Does a PortFacing FreePort city have linear prosperity decline from harbor to inland, or is it more complex (harbor districts: commercial prosperity high; adjacent residential: working class; distant new development: new wealth)?
- For Contested cities, how is the spatial position of the contested boundary established? It derives from which factions control what geographic features — but the boundary itself needs to be a deterministic geometric feature that Phase 5 can use to guide district layout.
**Layer 4:**
- How does `prosperity_delta` get communicated from the economic sim to the tile generator? Is it a per-district value stored in the save file alongside ChunkMutations, or is it derived at chunk-load time from current sim state?
- When perimeter treatment degrades over time (AbandonedZone maintenance ending), is that a sim-driven tile state change or does it require removing perimeter marker tiles? The former is simpler; the latter is more visually correct but touches locked structure.
---
## What the Brief Needs to Specify
For the planet-down cascade to produce politically legible worlds, the brief must lock:
1. **Layer 1 tagged feature output** — does the empty world generator produce a geographic feature tag layer, or is that re-derived at Layer 2?
2. **TerritorialStatus as Layer 2 post-processing** — confirm it's derived after settlement placement, not a separately authored or layered concept. This resolves OQ-R3-1 cleanly.
3. **Prosperity_delta architecture** — seed-derived `prosperity_index` is the locked baseline; economic sim produces a runtime `prosperity_delta`. This must be a named architectural decision before Layer 3 or Phase 5 are implemented, because it determines whether the ghost city effect is possible at all.
4. **Named cities exception scope** — the planet-down cascade makes most authoring unnecessary by deriving everything from geography + economics. The brief should specify what named cities still require: `political_archetype_override`, `prosperity_override`, and landmark names for major cities. That scope is small. Everything else should be derived.
5. **AbandonedZone lifecycle** — who flags a settlement for ruination? The economic sim, or the lead authoring specific events? The former enables emergent history; the latter enables authored history. Probably both, with the sim as default and authored events as override.
---
*Paula — Round 4. Written 2026-04-30.*
@@ -0,0 +1,320 @@
---
title: "Round 1 Notes — Generation Cascade Workshop"
description: "Compiled Round 1 inventory: consensus, disagreements, gaps, open questions, and consolidated pipeline layer table"
type: workshop
status: active
workshop: generation-cascade
agent: qatux
round: 1
created: 2026-04-30
---
# Generation Cascade Workshop — Round 1 Notes
**Compiled by:** Qatux
**Source files:**
- `docs/workshops/generation-cascade/gestalt-round1.md`
- `docs/workshops/generation-cascade/tyre-round1.md`
- `docs/workshops/generation-cascade/miri-round1.md`
---
## Summary Verdict (All Three Agree)
The pipeline has a hard operational cliff at Layer 5 (atlas markers). Every layer above it is working production code. Every layer below it is either missing entirely or compiled stub types with no generation logic. The architecture is well-designed on paper; zero generation code exists. This is not a design workshop — it is an implementation audit with clear blockers.
---
## Consensus Points
### 1. Pipeline cliff location
All three participants independently arrived at the same finding: the atlas pipeline (galactic definitions → planetary heightmaps → city placement → city naming → economics) is complete and working. The cliff is between the atlas output and the first runtime layer (city → district decomposition). Everything below is unimplemented.
- **Gestalt:** "The atlas is the last fully-working pipeline layer."
- **Tyre:** "The pipeline has a hard cliff at Layer 5. Everything above it is working production code."
- **Miri:** (concurs, layered audit confirms all atlas layers as DONE)
### 2. WorldTier enum mismatch — hard blocker
Both Gestalt and Tyre independently flagged the same bug:
```rust
// Current code (generator.rs):
enum WorldTier { Peripheral, Connected, Core }
// Workshop-locked values (lead decision L-3, workshop-outcomes.md):
enum WorldTier { Epicenter, Regional, Backwater, Passage, Waypoint }
```
- **Gestalt:** "This is a blocker. #899 cannot be implemented correctly against the current enum."
- **Tyre:** "These are incompatible. The workshop canonical values must replace the current enum before the generation spike begins."
Miri did not independently flag this bug (worldbuilder scope did not include code audit of enum values) but it is corroborated by both technical participants.
### 3. City → District decomposition is missing and has no ticket
All three participants confirmed this layer is entirely absent — no design, no code, no types, no ticket. It is the first unlock for all downstream work.
- **Gestalt:** Layer 6 in her inventory. "No ticket formally covers this. #899 assumes this decomposition as a prerequisite but does not scope it."
- **Tyre:** Layer 5 in his inventory. "This is the first gap in the entire pipeline... The `DistrictContext` type on `DistrictSkeleton` is `pub type DistrictContext = String`."
- **Miri:** Confirms the city marker schema is insufficient for Phase 1 input (see Gap 3 below).
### 4. Phase 1 DistrictSkeleton — types compile, zero generation logic
All three confirmed the `DistrictSkeleton` struct compiles but every field requiring actual content or logic is a `pub type Foo = String` alias.
**Fields confirmed as String stubs (Tyre inventory):** `DistrictContext`, `SocietyProfileRef`, `ZoneDefinition`, `DistrictBoundaries`, `GuaranteeAuditResult`, `CorridorSpine`, `ChunkLayout`, `Era`, `EraModification`, `LandmarkSlot`, `AccessPoint`.
### 5. Phase 2 ChunkData tile generation — entirely missing
All three confirmed Phase 2 does not exist. `GeneratorChunkData = Vec<bool>`. No tile vocabulary (`TileId = String`). No code to fill a 64×64 tile grid from a `BlockSkeleton`.
- **Tyre:** "The heritage grammar overlay system does not exist as Rust code or TOML data."
- **Gestalt:** "There is no pipeline from DistrictSkeleton → actual walkable tile data."
### 6. Triangle system is more complete than the brief indicated
Both Gestalt and Tyre noted that the workshop brief labels `triangle.rs` as "stubs" — this is inaccurate. The file is substantially implemented production code.
- **Gestalt:** "The triangle system (`server/src/simulation/triangle.rs`) is substantially implemented — template schemas, triangle escalation tick system, NPC spawn patterns. Intra-template (#107) and cross-template (#108) triangle generation are both done."
- **Tyre:** "Layer 9 — Social Site Templates and Triangle System: DONE (runtime system, not wired to generator output)."
The gap is not the triangle system itself but the connection from `DistrictSkeleton.social_sites` template tags to runtime `TemplateOwnership` component spawning.
### 7. chunk_streaming.rs — architecturally ready, generator hookup is zero
Both technical participants confirmed the streaming system is sound and tested. The gap is purely that `WalkabilityMap::load_chunk()` creates blank walkable placeholders rather than calling any generator.
- **Tyre:** Architecture is sound. `load_chunk()` needs steps 14 (chunk → district lookup → skeleton → Phase 2 fill) added. None exist.
- **Gestalt:** "The system is ready for the generator hookup; it just needs the generator to exist."
### 8. #619 and #694 are not generation-dependent
Both Gestalt and Tyre independently reached the same position: character customization (#619) and character creation screen (#694) are Phase 4 work (Player Control Scheme, hand-authored 2-floor test map) and should not be formally blocked behind the generation pipeline.
- **Gestalt:** "These should NOT be formally blocked behind generation pipeline work. The player needs to look like something before the world needs to be generated."
- **Tyre:** "#619 and #694 can proceed... Phase 4, same rationale."
Miri did not address these tickets but noted "#694 Character creation screen — no generation pipeline dependency flagged."
### 9. #681 needs a stronger generation blocker
All three participants agreed (directly or by implication) that #681 (Apartment generator) requires a formal blocker behind Phase 1 district skeleton generation.
- **Gestalt:** "add formal blocker on #681 behind Phase 1 district skeleton working."
- **Tyre:** "#681 — Needs walkable generated world (Layers 5-8 complete). Should be explicitly blocked."
- **Miri:** Notes that #681 can produce a culturally-voiced apartment without heritage grammar, but a spatially differentiated apartment requires HeritageGrammarOverlay (Gap 2). Proposes two-pass sequencing: first pass against culture RON, second pass when overlay data exists.
### 10. NPC generation spike exists but is not production code
Tyre found `server/src/bin/generator_spike.rs` (Sprint 25) — a standalone binary that generates NPCs from `ZoneSpec + CultureProfile`. It is not wired to ECS or to the spatial pipeline.
- **Tyre:** "It is a standalone binary that bypasses ECS entirely... Its output is printed to stdout, not spawned into the ECS world."
- **Gestalt:** Did not independently identify the spike binary, but notes the NPC system as a "wiring problem, not a design or implementation gap."
---
## Disagreements
### Disagreement 1: Dev-day estimates for thinnest vertical slice
| Participant | Estimate | Breakdown |
|-------------|----------|-----------|
| **Tyre** | ~9 dev-days | WorldTier fix (0.5) + city decomp (2) + Phase 1 stages 1-2 (3) + missing struct fields (0.5) + Phase 2 minimal (2) + streaming wiring (1) |
| **Gestalt** | ~57 dev-days | WorldTier fix (0.5) + Layer 6 stub (0.5) + Phase 1 minimal (2) + Phase 2 minimal (2) + wiring (1) |
**Source of discrepancy:** Tyre's Phase 1 estimate (3 days) cites the workshop-outcomes.md estimate directly. Gestalt's Phase 1 estimate (2 days) uses a "minimal" scope that defers social sites, reservations, and guarantee audit entirely. Tyre also includes a separate line item for missing DistrictSkeleton fields that Gestalt folds into Phase 1. Neither estimate includes heritage grammar, NPC population, or guarantee audit — both agree those are outside the minimum slice.
No position has been locked. The discrepancy is ~24 days and will need resolution in Round 2.
### Disagreement 2: Whether #615 (Tycoon starting state) is generation-dependent
| Participant | Position |
|-------------|----------|
| **Tyre** | #615 should be blocked behind generation pipeline completion. "#615 Tycoon small business starting state — Needs walkable generated world." |
| **Gestalt** | #615 is content design, not generation-dependent. "Game content design, not generation-dependent." It feeds #681 but is not itself blocked by generation work. |
Miri did not take a position on #615's cascade placement.
**Note:** Both agree that #615 is a real dependency *of* #681 (apartment generator). The disagreement is whether #615 should itself be blocked behind generation pipeline completion, or whether it can proceed in parallel as content design work.
---
## Gaps — Items Raised by Fewer Than All Three Participants
### Gap A: Content gaps raised by Miri alone (not addressed by Gestalt or Tyre)
Miri audited the *content inputs* required by each pipeline layer. These gaps are orthogonal to the code gaps Gestalt and Tyre identified. All four are blocking at their respective layers.
**Gap A1 — Heritage root blend specification (CRITICAL, blocks Phase 1 social stage)**
No authored document establishes which heritage root blend applies to any system. The `SocietyProfileRef` stub cannot be resolved without this mapping.
- Proposed solution (Miri): Extend culture RON schema with `heritage_blend` field, or author a global `heritage-map.ron`. Provides Van Maanen's Star estimate: Iron(0.4), Stone(0.35), Tide(0.25).
- Existing `culture-van-maanens-star.ron` provides NPC voice/behavior data but does NOT include heritage root blend weights.
**Gap A2 — HeritageGrammarOverlay data files (CRITICAL, blocks Phase 2)**
The full specification for all 10 heritage roots exists in `miri-round4.md` §OQ-R4-D. No `heritage_grammar_overlay/*.ron` files have been authored. No `HeritageRoot` enum exists in `generator.rs`. No `ObjectTag` vocabulary exists.
- **Miri:** "This is the largest cultural content gap in the pipeline." The specification is complete; the work is translation from workshop spec to authored data.
- **Content authoring path:** Miri authors organizational/social principles → Araminta authors visual expression → both co-maintain `ObjectTag` vocabulary.
**Gap A3 — City marker schema insufficient for Phase 1 (MODERATE)**
Current markers.json city entries contain: `id`, `name`, `kind`, `center`, `population`. Ticket #899 references `primary_function`, `planet_class`, `settlement_pattern` as Phase 1 inputs — these fields do not exist.
- Tyre identified the missing coordinate translation (atlas pixel → sim tile) as a separate problem but did not raise the functional field gap.
- Miri raised two possible resolutions: (a) extend markers.json schema, or (b) derive fields from systems.db cross-reference. Designates this as a joint Miri/Tyre question.
**Gap A4 — Zone identity definitions for all DistrictType values (MODERATE)**
`ZoneDefinition = String` stub. The `DistrictType` enum has 9 values. Only 2 Van Maanen's Star zone files exist (`van-maanens-star-rural-zone.ron`, `van-maanens-star-industrial-zone.ron`). No coverage for LogisticsHub, Administrative, Entertainment, MixedUse, Transit, Specialized.
### Gap B: Atlas coordinate system translation (Tyre only)
Tyre identified a specific unresolved technical gap not mentioned by Gestalt or Miri:
> "Atlas coordinates are (row, col) in a 512×256 pixel grid. A district is 512×512 sim tiles = 256×256 visual tiles = 256m. The ratio between atlas pixels and sim tiles is undefined and unimplemented."
This is a prerequisite for the city → district decomposition layer. No translation formula or specification exists.
### Gap C: Missing DistrictSkeleton fields (Tyre only)
Tyre identified three fields present in the workshop spec (workshop-outcomes.md) that are absent from the current `DistrictSkeleton` struct in `generator.rs`:
- `vertical_structure: VerticalStructure` (D-READY-11)
- `breach_only_zones: Vec<ZoneId>` (Tier 2 guarantee audit)
- `derived_analysis: DerivedDistrictAnalysis` (Phase 1 computed, Miri/Gestalt source)
Gestalt did not independently identify these missing fields. Tyre estimates 0.5 dev-days to add them as stubs.
### Gap D: Van Maanen's Star naming ambiguity (Miri only)
Miri flagged a wiki documentation inconsistency: "Van Maanen's Star" is associated with two system entries (GJ-35/Vuurkloof — the real-astronomy white dwarf; GJ-280B/Sova — the K-type system with Station Sova). The culture RON file `culture-van-maanens-star.ron` is validated and correct. The naming ambiguity is a wiki gap, not a content error.
- **Miri's reading:** Vuurkloof is the founding culture; Station Sova is where that culture manifests in the current setting. A formal `wiki/cultural-groups/van-maanens-star.md` entry would close this.
- Neither Gestalt nor Tyre addressed this; it is a worldbuilding/documentation item.
### Gap E: Economics → generation bridge design (Gestalt only)
Gestalt identified the economics layer as rich and immediately usable as a district generation input, but no such bridge code exists. Proposed principle:
> "Economics drives district type; culture drives zone palette; both come from systems.db data that already exists."
Gestalt proposed Layer 6 (city → district decomposition) as the natural integration point. Neither Tyre nor Miri addressed this bridge design in their Round 1 files.
---
## Open Questions
All open questions raised across the three Round 1 files, attributed to source:
| ID | Question | Source | Priority |
|----|----------|--------|----------|
| OQ-R1-A | Heritage root count: 7 (Mellanie's culture authoring guide) vs. 10 (Miri Round 4). Which is canonical? Must be resolved before HeritageGrammarOverlay files can be authored. | Miri | **CRITICAL** |
| OQ-R1-B | `SocietyProfileRef` resolution: culture RON filename (e.g. `"van-maanens-star"`) or structured reference including heritage blend weights? Affects both the type definition and the culture RON schema. | Miri | **CRITICAL** |
| OQ-R1-C | City marker schema: does Phase 1 read `primary_function` / `planet_class` / `settlement_pattern` from markers.json entries (requires atlas generator update), or derive from systems.db cross-reference by position (requires lookup code)? | Miri + Tyre | **HIGH** |
| OQ-R1-D | Atlas coordinate translation: what is the mapping formula from atlas pixel coordinates (512×256 grid) to world sim-tile coordinates (512×512 per district)? No spec or code exists. | Tyre | **HIGH** |
| OQ-R1-E | Thinnest vertical slice effort: Tyre estimates ~9 dev-days; Gestalt estimates ~57. What is the minimum Phase 1 scope (does it include Stages 35, or Stages 12 only)? | Gestalt + Tyre | HIGH |
| OQ-R1-F | #615 (Tycoon starting state) cascade position: block behind generation pipeline (Tyre) vs. proceed as content design (Gestalt)? | Gestalt + Tyre | MEDIUM |
| OQ-R1-G | `wiki/cultural-groups/` population timing: author now alongside heritage content (Gaps A1-A2), or defer to Phase 3? | Miri | LOW |
| OQ-R1-H | Pre-workshop tickets (#100, #156, #160) were scoped before the confirmed architecture. Do they get re-scoped in this workshop or handled as separate post-workshop cleanup? | Gestalt | LOW |
---
## Consolidated Pipeline Layer Table
Unified numbering across all three participants' inventories. Where participants used different layer numbers, this table uses a canonical sequence.
| Layer | Name | Gestalt | Tyre | Miri | Unified Status |
|-------|------|---------|------|------|----------------|
| 1 | Galactic / system definitions | DONE | DONE | DONE | **DONE** |
| 2 | Planetary heightmap simulation | DONE | DONE | DONE | **DONE** |
| 3 | Atlas markers — city placement, infrastructure | DONE | DONE | DONE | **DONE** |
| 4 | City naming (Gemma) | DONE | DONE | DONE | **DONE** |
| 5 | Economics layer — supply chains, brands, trade | DONE | (embedded L1) | DONE | **DONE** |
| 6 | **City → District decomposition** | MISSING | MISSING | MISSING (schema gap) | **MISSING — CRITICAL. No ticket, no code, no design.** |
| 7 | Phase 1: DistrictSkeleton generation | STUB | STUB | STUB | **STUB — types compile, zero generation logic. WorldTier enum wrong.** |
| 7a | — Stage 1: Classification (WorldTier, ComplexityTier, SettingType) | STUB | STUB | no design rules authored | STUB |
| 7b | — Stage 2: Block grid (DistrictLayoutMode, BlockSkeleton ×16) | STUB | STUB | no layout probability tables | STUB |
| 7c | — Stage 3: Reservations (MultiBlockReservation, terminals) | STUB | STUB | — | STUB |
| 7d | — Stage 4: Social sites + NPC (SocialSitePlacement, triangles) | STUB | STUB | 3/230+ culture files; no heritage blends | STUB |
| 7e | — Stage 5: Guarantee audit (GuaranteeAuditResult) | STUB | STUB | — | STUB (`String` alias) |
| 8 | Society profile / culture resolution | PARTIAL (#679 done, not wired to skeleton) | PARTIAL (spike binary only) | PARTIAL (3 RON files, no heritage blends) | **PARTIAL — wiring gap + content gap** |
| 9 | Phase 2: ChunkData tile generation | MISSING | MISSING | MISSING | **MISSING — CRITICAL. No code, no tile vocabulary.** |
| 10 | Heritage grammar overlay application | MISSING | MISSING | MISSING (no data files for any root) | **MISSING — design complete (miri-round4.md spec), no Rust type, no data** |
| 11 | NPC population / manifest seeding | PARTIAL (triangle system full; NpcManifest missing) | PARTIAL (spike, no ECS wiring) | PARTIAL (culture RON operational, no heritage blend) | **PARTIAL — machinery exists, pipeline connection missing** |
| 12 | Chunk streaming → generator hookup | STUB (streaming works; generator call is blank) | PARTIAL (load/unload works; fill is wrong) | N/A | **PARTIAL — streaming architecture ready, generator call absent** |
**Key:** Layer 6 and Layer 9 are the two critical missing gaps. Layer 7 has types but no logic. Layers 8, 11, 12 are partial with clear wiring gaps. Everything at Layer 10 is zero.
---
## New Tickets Proposed — Summary by Participant
### Gestalt's proposed new tickets
| Priority | Scope | Blocked by |
|----------|-------|-----------|
| Blocker | Fix WorldTier enum in generator.rs | #897 |
| Critical | Layer 6: City → District decomposition | None (after WorldTier fix) |
| Critical | Phase 2: Basic chunk tile generator | #899 |
| High | Connect chunk_streaming to Phase 2 generator | Phase 2 ticket |
| High | Wire society profile (#679) to DistrictSkeleton | #899 |
| Medium | Re-scope #156, #100 against confirmed architecture | Phase 2 ticket |
### Tyre's proposed new tickets
| Priority | Scope | Effort | Blocked by |
|----------|-------|--------|-----------|
| 1 | Fix WorldTier enum values in generator.rs | 0.5d | Nothing |
| 2 | City-to-district decomposition — spec and implementation | 2d | Nothing |
| 3 | DistrictSkeleton Phase 1 generator (Stages 12) | 3d | Tickets 1, 2 |
| 4 | Add missing DistrictSkeleton fields (`vertical_structure`, `breach_only_zones`, `derived_analysis`) | 0.5d | Ticket 3 |
| 5 | ChunkData Phase 2 minimal fill | 2d | Ticket 3 |
| 6 | Wire generator to chunk_streaming (`DistrictMap` resource + dispatch) | 1d | Tickets 2, 3, 5 |
### Miri's proposed new tickets (content)
| Priority | Scope |
|----------|-------|
| Critical | Heritage root blend mapping — extend culture RON schema, author Van Maanen's Star blend |
| Critical | HeritageGrammarOverlay data files — 10 heritage roots from miri-round4.md spec |
| High | ObjectTag vocabulary — joint Miri + Araminta authoring |
| High | Zone identity definitions — one spec per DistrictType value |
| Medium | Van Maanen's Star cultural group wiki entry |
| Medium | City marker schema extension — coordinate with Tyre |
---
## Existing Ticket Positions — All Three Participants
| Ticket | Title | Gestalt | Tyre | Miri |
|--------|-------|---------|------|------|
| #615 | Tycoon small business starting state | Not generation-dependent; feeds #681 | Should be blocked behind generation pipeline | No position taken on cascade placement |
| #616 | Economic verb vocabulary design | Content design; no pipeline dependency | Can proceed | No pipeline dependency |
| #619 | Full character customisation | NOT generation-dependent; Phase 4 UI | NOT blocked by generation pipeline | No pipeline dependency flagged |
| #681 | Apartment generator (server) | Add Phase 1 skeleton as formal blocker | Should be blocked behind vertical slice | Can proceed for culturally-voiced version; heritage spatial refinement is a second pass |
| #682 | Apartment visual rendering (client) | Blocked by #681 — correct, no changes | Blocked by #681 | Blocked by #681 |
| #694 | Character creation screen | NOT generation-dependent; Phase 4 UI | NOT blocked by generation pipeline | No pipeline dependency flagged |
| #899 | District skeleton generator — Phase 1 | Correct scope; misses Layer 6 and WorldTier fix | Blocked by city decomp ticket + WorldTier fix | Blocked by #897 + heritage blend content |
---
## For Round 2 Attention
The following items from Round 1 carry unresolved positions that Round 2 should address:
1. **OQ-R1-A (Heritage root count)** — Must be resolved before any heritage content can be authored. Affects Miri and Araminta's work scope.
2. **OQ-R1-B (SocietyProfileRef type)** — Affects both the Rust type design and culture RON schema. Tyre and Miri should align on this before #899 scoping.
3. **OQ-R1-C (City marker schema)** — Joint Miri/Tyre decision. Determines whether `generate_atlas.py` must be extended.
4. **OQ-R1-D (Atlas coordinate translation)** — No participant proposed a formula. Tyre owns this technically.
5. **OQ-R1-E (Vertical slice scope and effort)** — Gestalt and Tyre estimates diverge by 24 days. Agree on minimum Phase 1 stages in Round 2.
6. **OQ-R1-F (#615 cascade position)** — Gestalt and Tyre disagree. Needs a position for the ticket dependency graph.
7. **Gap E (Economics → generation bridge design)** — Gestalt proposed a design principle but it has no D-record candidate yet. Round 2 should decide whether this produces a D-record.
---
*Notes compiled by Qatux from Round 1 source files. All positions attributed to their source participant. No editorial opinions added.*
@@ -0,0 +1,375 @@
---
title: "Round 2 Notes — Generation Cascade Workshop"
description: "Compiled Round 2 proposals: consensus, Paula's spatial narrative additions, disagreements, D-record candidates, ticket dependency convergence, and open questions for Round 3"
type: workshop
status: active
workshop: generation-cascade
agent: qatux
round: 2
created: 2026-04-30
---
# Generation Cascade Workshop — Round 2 Notes
**Compiled by:** Qatux
**Source files:**
- `docs/workshops/generation-cascade/gestalt-round2.md`
- `docs/workshops/generation-cascade/tyre-round2.md`
- `docs/workshops/generation-cascade/paula-round2.md`
**Scope:** Spatial pipeline only — from atlas city markers to walkable rendered tiles. Heritage grammar, NPC population, social sites, culture overlays, and #615 are out of scope per lead directive.
---
## Summary
Round 2 achieved strong convergence on implementation order, effort, and the stored-vs-derived boundary. The two key technical open questions from Round 1 (OQ-R1-C and OQ-R1-D) are now resolved by both technical participants independently reaching the same conclusions. Paula introduced three new spatial narrative fields and a naming register system that neither Gestalt nor Tyre addressed — these require Round 3 disposition before Phase 2 can be scoped. Effort estimates converged to ~67 days once heritage grammar contamination was removed from Tyre's Round 1 figure.
---
## Out-of-Scope References — Flagged
Two references in the Round 2 files touch heritage or cultural territory. Per scope rules, these are flagged but not incorporated:
1. **Paula §5 on #615:** Paula references "OQ-R1-B resolved (SocietyProfileRef type)" as a dependency for #615. `SocietyProfileRef` is heritage territory (culture file resolution). The reference is used to position #615's unblocking condition, not to reopen heritage design — but the dependency chain Paula describes would require an out-of-scope decision. Not incorporated into consensus.
2. **Paula §2 — Naming system:** The five naming *registers* themselves (`Commemorative | Functional | Aspirational | Folk | Geographic`) are spatial parameters derivable from `(DistrictType, WorldTier)`. The *lookup table* that maps registers to types is in scope as a spatial decision. However, Paula notes that "the names are drawn from culture pool" for seeded generation — name pools are heritage/cultural territory. The register taxonomy and lookup table are captured here; the name pool population is flagged as out of scope for this workshop.
---
## Consensus Points
### 1. Effort estimates converged: ~67 dev-days
Round 1 gap (Gestalt ~57d vs. Tyre ~9d) was caused by heritage grammar contamination in Tyre's estimate. With spatial-only scope:
- **Gestalt Round 2:** 5.5 days (explicit line-item breakdown)
- **Tyre Round 2 revised:** 67 days (conservative; includes type migration friction)
Gestalt's 5.5d floor represents minimum friction. Tyre's 67d includes explicit allowance for `String`-stub-to-real-type migration surprises during compilation. Paula (§5) explicitly supports the Gestalt floor estimate and Stages 12 deferral position.
**Converged answer:** ~6 days under reasonable conditions. 78 days if String-stub migrations surface unexpected breadth.
### 2. OQ-R1-C resolved: do not extend markers.json
Both Gestalt and Tyre independently reached the same answer:
> `primary_function`, `planet_class`, and `settlement_pattern` are read from `systems.db` bodies table by cross-reference on `body_id`. `generate_atlas.py` does not need modification.
- **Gestalt:** "Do not extend markers.json. Instead, resolve via systems.db cross-reference using body_id... keeps markers.json as a pure spatial-plus-naming store."
- **Tyre:** "`primary_function`, `planet_class`, `settlement_pattern` are available in systems.db bodies table and do NOT need to be added to markers.json."
**Status: RESOLVED. No markers.json schema changes. City decomposition reads systems.db bodies table directly.**
### 3. OQ-R1-D resolved: no pixel-to-sim-tile formula needed
Tyre provided the architectural resolution:
> "The atlas grid is a display coordinate system for the atlas UI. It is not a world-space coordinate for the game simulation. The walkable world uses a city-local coordinate system starting at (0, 0)."
Atlas pixel coordinates are UI-only (city dot placement on planetary map). The walkable world uses city-local sim-tile coordinates. Districts in a city are placed at `(col × 512, row × 512)` sim tiles in city-local space. No conversion from atlas pixels to sim tiles is needed or meaningful.
Gestalt's Round 2 acknowledged the same conclusion: "For the minimum slice, we don't need this — we spawn one district at world origin." Gestalt deferred the multi-city coordinate question to Step 2b / Round 3; Tyre provided the answer: it's city-local grid, not atlas-pixel-derived.
**Status: RESOLVED. Atlas coordinates are UI-only. No formula needed. City-local sim-tile coordinates from (0,0).**
### 4. Seed-vs-stored boundary: atlas markers are the last stored layer
Both Gestalt and Tyre independently reached the same boundary definition:
**Stored (authored or generated-then-committed):**
- Star system and body definitions (wiki + systems.db)
- Economic data, corporations (systems.db economics tables)
- City positions, populations, infrastructure (markers.json + systems.db atlas tables)
- City names (markers.json, Gemma-filled, curated)
- Chunk mutations / DamageOverlay (save file)
**Seed-derived (never stored, always re-derived from world_seed):**
- District count and positions per city
- DistrictSkeleton (all fields)
- BlockSkeleton per district
- ChunkData tile grid
- Building footprints, door positions
- **Gestalt:** "At the boundary between atlas markers and city → district decomposition (between Layer 5 and Layer 6)."
- **Tyre:** "The boundary is between Layer 4 and Layer 6... markers.json city entries are the stored anchor. Below that, the world is fully deterministic from world_seed."
**Status: CONSENSUS. Atlas markers are the last stored layer. Save files store world_seed + markers_snapshot + ChunkMutations only.**
### 5. SeedChain design: FNV-1a child_seed
Tyre proposed the concrete seed derivation mechanism. Gestalt's formula (`world_seed XOR fnv1a(city_id) XOR district_index`) uses the same approach. Consistent with D-010 (FNV-1a determinism mandate).
**Tyre's proposed canonical form:**
```rust
fn child_seed(parent: u64, discriminant: u64) -> u64 {
let mut h = parent ^ 0xcbf29ce484222325;
for byte in discriminant.to_le_bytes() {
h ^= byte as u64;
h = h.wrapping_mul(0x100000001b3);
}
h
}
district_seed = child_seed(world_seed, district_id)
chunk_seed = child_seed(district_seed, (cx as u64) << 32 | (cy as u64))
```
Properties: any district or chunk can be generated in isolation without generating neighbors first. Generation is parallelizable. No global state required.
### 6. Phase 1 minimum scope: Stages 12 only
All three participants agree Phase 2 tile generation requires only Stages 1 and 2 of Phase 1. Stages 35 are deferred.
- **Gestalt:** Stages 12 mandatory. "Fields deferred: reservations, corridors, social_sites, boundaries, guarantee_audit, zone_palette, society_profile, access_points."
- **Tyre:** "Stages 3 (reservations), 4 (social sites), and 5 (guarantee audit) are deferred — they produce spatial enrichment but are not required for a walkable tile."
- **Paula (§5):** "I'd formally support deferring Stages 35 until after the first walkable district is demonstrably legible."
### 7. Implementation order (all three agree)
The five-step sequential dependency chain is consistent across Gestalt and Tyre. Steps cannot be parallelized until Step 3 produces a compilable skeleton:
```
Step 1: Fix WorldTier enum (0.5d)
└─▶ Step 2a: Single-district decomposition stub (0.5d)
└─▶ Step 3: Phase 1 Stages 1-2 generation (1.5-2d)
├─▶ Step 4: Phase 2 minimal tile generator (1.5d)
│ └─▶ Step 5: Chunk streaming wiring (0.5-1d)
│ └─▶ [MINIMUM VIABLE WALKABLE WORLD]
└─▶ Step 2b: Full city decomposition + district grid (1d, follow-on)
```
### 8. GeneratorChunkData type must be upgraded
Both Gestalt and Tyre note that `pub type GeneratorChunkData = Vec<bool>` cannot carry tile IDs and must become a proper struct. Tyre specifies the form:
```rust
pub struct GeneratorChunkData {
pub tiles: Vec<TileEntry>, // 64×64 = 4096 entries
}
pub struct TileEntry {
pub tile_id: TileId,
pub walkable: bool,
}
```
Gestalt concurs that `TileId` per cell is needed for zone-tinted floor tiles. The type change is a prerequisite for Phase 2.
### 9. Existing tickets consensus
All three participants (where they addressed tickets) reached identical positions:
| Ticket | Consensus position |
|--------|--------------------|
| #619 | NOT generation-dependent. Phase 4 work. No generation blockers. |
| #694 | NOT generation-dependent. Phase 4 work. No generation blockers. |
| #681 | Add formal blocker behind "Minimum Viable Walkable World" milestone. |
| #682 | Correctly blocked by #681. No changes. |
| #616 | No generation dependency. Can proceed. |
| #156 | Close/supersede. New Phase 2 ticket replaces it. |
| #100 | Close/fold into chunk streaming wiring ticket. |
| #899 | Rescope: Stages 12 only. Remove Stage 45 from scope. Add dependency on WorldTier fix + new city decomp ticket. |
| #615 | Out of scope per lead directive. No position. |
---
## Paula's Spatial Narrative Proposals — Mapping to Gestalt/Tyre
Paula introduced three new concepts not addressed by Gestalt or Tyre. Each requires Round 3 disposition before it can be confirmed as in-scope for the minimum slice.
### Paula Proposal P1: `prosperity_index: f32` (new field)
A single 0.01.0 scalar on `DistrictSkeleton` that drives tile decay probability, road width, building density, and green space allocation. Derived from `WorldTier` + `DistrictType` + city economic profile from systems.db. No authoring required.
| Prosperity band | Tile decay | Road width | Building density | Green space |
|-----------------|-----------|------------|-----------------|-------------|
| 0.00.2 (subsistence) | 4060% damaged | Narrow | Very high | None |
| 0.20.4 (working) | 2035% | Narrow-medium | High | Minimal |
| 0.40.6 (middle) | 520% | Medium | Medium | Small plazas |
| 0.60.8 (affluent) | 15% | Medium-wide | Lower (setbacks) | Parks |
| 0.81.0 (elite) | <1% | Wide/boulevard | Low (wasted space = luxury) | Gardens |
**Gestalt/Tyre accommodation:** Neither includes `prosperity_index` in their Stage 1 classification. Adding it is architecturally clean — it's a derived field with the same inputs as other Stage 1 fields. However, it changes Phase 2 tile generation (density-driven building placement is in Tyre's algorithm; Paula's version connects that density to prosperity). **Round 3 must decide: is `prosperity_index` in scope for the minimum slice?**
Paula's argument for inclusion: "If prosperity_index and perimeter_treatment aren't on DistrictSkeleton before Phase 2 is implemented, tile generation will produce spaces that are spatially indistinguishable regardless of district type — a simulation fidelity problem that's expensive to retrofit."
### Paula Proposal P2: `perimeter_treatment: PerimeterTreatment` (new field)
New enum on `DistrictSkeleton`:
```
Open // no boundary markers; permeable
Fenced // visible boundary, no control
Walled // physical barrier, uncontrolled gaps
Gated // checkpoints on main arteries, open on minor
Checkpoint // all entry points monitored
```
Derived from `DistrictType` + `WorldTier` + `prosperity_index`. Manifests as boundary marker tiles at `access_points` in Phase 2.
**Gestalt/Tyre accommodation:** `AccessPoint` is already in `DistrictSkeleton` as a stub (`pub type AccessPoint = String`). `perimeter_treatment` extends the access system with political character. Neither Gestalt nor Tyre proposed this field. **Round 3 must decide whether this is in scope for the minimum slice.**
Paula's argument: "The approach to a district is the first story beat. A player walking toward a Checkpoint district understands access control before entering. No tile variation needed — just the presence of the boundary marker tile."
### Paula Proposal P3: `political_archetype` (authored per city)
Five archetypes for city-level authoring (one field per city in atlas/systems.db):
- `CompanyTown` — one corporation dominates; homogeneous district type distribution
- `AdminCapital` — government/institutional core with support districts
- `FreePort` — Transit and MixedUse dominant; contested/heterogeneous
- `Contested` — two or more factions with visible boundary zones
- `OrganicGrowth` — no dominant power; district types mix without hierarchy
Paula notes this "drives the district type distribution for Layer 6" and is derivable from systems.db economics data for most cities, but named cities warrant sanity-checking. This is "a small authoring task, not a large one."
**Gestalt/Tyre accommodation:** Neither Gestalt nor Tyre proposed `political_archetype`. Gestalt's decomposition formula (`DistrictType from body.economic_role + city.kind`) approximates this without the concept explicitly. Paula's proposal is a named abstraction over the same data. **Round 3 must decide whether to formalize this as a named field or keep it implicit in the decomposition formula.**
### Paula Proposal P4: Naming Registers (in-scope component only)
Paula proposes five naming registers that determine what *kind* of name a district or street receives:
| Register | Pattern | Typical districts |
|----------|---------|-----------------|
| Commemorative | Person's/institutional name | Administrative, elite Residential |
| Functional | What happens here | Industrial, Transit, LogisticsHub |
| Aspirational | Abstract positive noun | New Commercial, MixedUse growth |
| Folk | Colloquial/ironic | Old Residential, Backwater |
| Geographic | Terrain feature | Pre-political, oldest settlements |
The register lookup table `(DistrictType, WorldTier) → register` is a spatial/structural decision — in scope for this workshop. The name pools that populate each register are cultural content — **out of scope for this workshop**.
Neither Gestalt nor Tyre addressed district naming. Paula's register system adds no new DistrictSkeleton fields but informs Phase 2 street labeling. **Round 3 must decide: does the naming register lookup table need to be locked before Phase 2, or is it a post-walkable enhancement?**
---
## D-Record Candidates
All candidates identified across the three Round 2 files. Sources attributed.
### D-C1: Generation Pipeline Layer Definitions (Gestalt)
Canonical spatial pipeline layer sequence:
| Layer | Name | Input | Output | Storage policy |
|-------|------|-------|--------|----------------|
| 15 | Atlas layers (existing) | Wiki + authored content | systems.db, markers.json | Explicitly stored |
| 6 | City → District decomposition | City entry + systems.db bodies | DistrictPlacement vec | Seed-derived, not stored |
| 7 | Phase 1: DistrictSkeleton | DistrictPlacement | DistrictSkeleton (Stages 12) | Seed-derived, not stored |
| 8 | Phase 2: ChunkData fill | BlockSkeleton + district seed | 64×64 TileEntry grid | Seed-derived, cached in session |
| 9 | Chunk streaming | Player position | On-demand Phase 2 call | Architecture |
### D-C2: Stored-vs-Derived Transition (Gestalt + Tyre)
Atlas marker data (city positions, populations, road topology, city names) is explicitly stored. Everything at Layer 6 and below is seed-derived at runtime. Save files store: `world_seed`, markers snapshot hash, and `ChunkMutations` (player-modified chunks only). Changing generation algorithms between a player's saves is a breaking change requiring migration versioning.
### D-C3: Phase 1 Minimum Spatial Scope (Gestalt, confirmed by all)
Phase 1 has two mandatory spatial stages for Phase 2 prerequisite:
- **Stage 1 (mandatory):** Classification — WorldTier, ComplexityTier, SettingType, DistrictType, LayoutMode, z_levels
- **Stage 2 (mandatory):** Block grid — 4×4 BlockSkeleton with ZoningType, density_pct, seed
Stages 35 (Reservations, Social sites, Guarantee audit) are enrichment — they add value to an already-walkable world but are not prerequisites for walkable tile generation.
### D-C4: City → District Decomposition Formula (Gestalt + Tyre)
- District count: `max(1, floor(city.population / 50_000))`, log-scaled for large cities, capped at WorldTier ceiling
- WorldTier district ceiling: Epicenter → uncapped; Regional → 8; Backwater → 4; Passage → 2; Waypoint → 1
- Capital cities: +1 bonus district
- Domed/cave settlement: fixed 1 district regardless of population
- Seed chain: `district_seed = child_seed(world_seed, district_id)` via FNV-1a (D-010)
- DistrictType: derived from `body.economic_role` + `city.kind` + district index within city
- Cross-reference source: systems.db bodies table (not markers.json)
### D-C5: City-Local Coordinate System for Districts (Tyre)
The atlas coordinate system (512×256 pixel grid) is a UI display system only. The walkable world uses city-local sim-tile coordinates. A city's (row, col) atlas position is used only for rendering the city dot on the planetary map UI. All district geometry is expressed in city-local coordinates: district at grid position (col, row) occupies world origin `(col × 512, row × 512)` sim tiles. No pixel-to-sim-tile conversion formula exists or is needed.
### D-C6: SeedChain Algorithm (Tyre)
All child seeds in the spatial pipeline are derived via FNV-1a from a parent seed and a discriminant:
```rust
fn child_seed(parent: u64, discriminant: u64) -> u64 // (FNV-1a body above)
```
This ensures: any district or chunk can be generated in isolation; generation is parallelizable; no global state required. Complies with D-010 (FNV-1a determinism mandate). Any deviation from `SimRng` or FNV-1a for randomness in the generation pipeline is prohibited.
### D-C7: GeneratorChunkData Type Upgrade (Tyre, confirmed Gestalt)
`pub type GeneratorChunkData = Vec<bool>` is insufficient — it carries only walkability. It must be replaced with a struct carrying per-tile `TileId` and pre-computed walkability. Minimum viable tile vocabulary for the spatial pipeline: `floor_street`, `floor_interior`, `wall` (three types). Visual variety and heritage grammar apply tile variation in subsequent phases.
### D-C8 (Paula proposal): Spatial Class Legibility Requirements
Three fields are minimum requirements for a district to be narratively legible to a player traversing it without NPC interaction or UI overlay:
- `prosperity_index: f32` — drives tile decay, road width, density, green space
- `perimeter_treatment: PerimeterTreatment` — enum: `Open | Fenced | Walled | Gated | Checkpoint`
- `layout_mode: DistrictLayoutMode` — political geometry signal (planned power vs. ungoverned growth)
Both `prosperity_index` and `perimeter_treatment` are derived from `DistrictType + WorldTier + city economic profile`. No authoring required for unnamed cities.
**Note:** D-C8 is proposed by Paula. Gestalt and Tyre did not address these fields. Round 3 must decide whether to adopt, modify, or defer this D-record.
---
## Open Questions for Round 3
Questions resolved in Round 2 are marked CLOSED. New questions are from this round's analysis.
| ID | Question | Source | Status | Priority |
|----|----------|--------|--------|----------|
| OQ-R1-A | Heritage root count: 7 vs 10 | Miri R1 | Out of scope | — |
| OQ-R1-B | SocietyProfileRef type | Miri R1 | Out of scope | — |
| OQ-R1-C | City marker schema approach | Miri/Tyre R1 | **CLOSED** — cross-ref systems.db, no markers.json changes | — |
| OQ-R1-D | Atlas coordinate translation | Tyre R1 | **CLOSED** — no formula; city-local coords | — |
| OQ-R1-E | Vertical slice effort | Gestalt/Tyre R1 | **CLOSED** — ~6 days (spatial scope only) | — |
| OQ-R1-F | #615 cascade position | Gestalt/Tyre R1 | **CLOSED** — out of scope per lead directive | — |
| OQ-R2-1 | Should `prosperity_index` and `perimeter_treatment` be added to DistrictSkeleton for the minimum slice, or deferred? | Paula R2 | Open | **HIGH** |
| OQ-R2-2 | Should `political_archetype` be a formal authored field per city, or kept implicit in the decomposition formula? | Paula R2 | Open | **HIGH** |
| OQ-R2-3 | Phase 2 tile algorithm: Gestalt's door-per-block-edge vs. Tyre's density-driven rectangle placement. Which is implemented first? | Gestalt/Tyre R2 | Open | **HIGH** |
| OQ-R2-4 | Step 2b (full N-district city layout): in scope for the minimum slice, or strictly a follow-on ticket? Gestalt: defer. Tyre: includes it in architecture. | Gestalt/Tyre R2 | Open | MEDIUM |
| OQ-R2-5 | Naming register lookup table: is this locked before Phase 2, or post-walkable enhancement? | Paula R2 | Open | MEDIUM |
| OQ-R2-6 | `prosperity_index`: authored for named cities (Van Maanen's Star capital) or always derived from systems.db? | Paula R2 | Open | MEDIUM |
| OQ-R2-7 | Naming register table ownership (who authors `(DistrictType, WorldTier) → register` mapping)? | Paula R2 | Open | LOW |
---
## Ticket Dependency Graph — Converged
From Gestalt Round 2 §6, corroborated by Tyre's architecture:
```
NEW: Fix WorldTier enum (generator.rs) [0.5d]
├──▶ NEW: City → district decomp — single district (Step 2a) [0.5d]
│ │
│ └──▶ #899 (rescoped): Phase 1 Stages 1-2 only [1.5-2d]
│ │
│ ├──▶ NEW: Phase 2 — minimal chunk tile gen [1.5d]
│ │ │
│ │ └──▶ NEW: Chunk streaming hookup [0.5-1d]
│ │ │
│ │ └──▶ [MINIMUM VIABLE WALKABLE WORLD]
│ │ │
│ │ └──▶ #681 ADD BLOCKER HERE
│ │ └──▶ #682 (already blocked by #681)
│ │
│ └──▶ NEW: Missing DistrictSkeleton fields [0.5d]
│ (vertical_structure, breach_only_zones, derived_analysis as stubs)
└──▶ NEW: Full city decomp + district grid (Step 2b, follow-on) [1d]
(not required for minimum slice; unblocks multi-district cities)
```
**New tickets needed (identified across all three participants):**
1. Fix WorldTier enum in generator.rs
2. City → district decomposition — single district (Step 2a)
3. #899 rescoped: Phase 1 Stages 12 only
4. Add missing DistrictSkeleton fields as stubs (Gap C)
5. Phase 2 — minimal chunk tile generator (new)
6. Chunk streaming → generator wiring (new)
7. Full city decomposition + district grid layout (Step 2b, follow-on)
8. Close/supersede #156 (Procedural district filler)
9. Close/fold #100 (Ungenerated → instantiation)
---
*Notes compiled by Qatux from Round 2 source files. All positions attributed to source participant. No editorial opinions added.*
@@ -0,0 +1,396 @@
---
title: "Round 3 Notes — Generation Cascade Workshop"
description: "Compiled Round 3 outputs: Phase 3 spatial pipeline architecture, tooling/runtime split consensus, layer alignment, Paula's political signals, Burnelli-Sheldon's economics bridge, D-record candidates, and open questions"
type: workshop
status: active
workshop: generation-cascade
agent: qatux
round: 3
created: 2026-04-30
---
# Generation Cascade Workshop — Round 3 Notes
**Compiled by:** Qatux
**Source files:**
- `docs/workshops/generation-cascade/gestalt-round3.md`
- `docs/workshops/generation-cascade/tyre-round3.md`
- `docs/workshops/generation-cascade/paula-round3.md`
- `docs/workshops/generation-cascade/burnelli-sheldon-round3.md`
**Scope:** Phase 3 spatial pipeline — regional maps, city footprints, road networks, biome grids, station topology, settlement hierarchy. NOT Phase 5 tile generation.
---
## Summary
Round 3 achieved structural consensus on the most important architectural question: Phase 3 is tooling (Python, offline, committed to systems.db), Phase 5 is runtime (Rust, seed-derived). Both are fully parallelizable during development. The layer inventories from Gestalt and Tyre are substantially aligned but not identical — Gestalt has six named layers (AF), Tyre has five (AE). The principal gap is that Tyre's schema and struct definitions omit `TerritorialStatus`, `FoundingOrientation`, `MaintenanceAuthority`, and the sub-settlement / `PoliticalTether` architecture that Gestalt accepts and accommodates from Paula. Burnelli-Sheldon provides concrete economic field mappings, a 10×9 district type weight table, a minimum six-field read set, and three real-body case studies demonstrating differentiation. Effort estimates diverge (~7d Tyre vs. ~9.5d Gestalt) for quantifiable reasons. All seven Round 2 open questions are now resolved.
---
## Out-of-Scope References — Flagged
1. **Paula §5 — name pool population:** Paula's naming strata section describes when `common_name` is populated as a spatial/political trigger condition (in scope), but notes "names drawn from culture pool" for the seeded generation content. The trigger logic and struct (`RegionalFeatureName`) are spatial decisions and captured here. The name pools themselves are cultural territory — out of scope for this workshop, same boundary established in Round 2.
2. **Burnelli-Sheldon §3 — `shadow_economy_access` and `collection_efficiency`:** These economics fields drive a MixedUse fringe district placement with specific prosperity floor and perimeter treatment. This is district *classification* (Phase 3/Phase 5 Stage 1), not tile generation. Within scope as far as district type assignment goes. The MixedUse fringe is a district-level output, not a tile layout.
3. **No Phase 5 tile generation crept back in.** All four participants maintain a clean boundary. Burnelli-Sheldon's building archetype signals (bulk_class → building type vocabulary) describe *what kinds* of buildings populate a district, which is Phase 5 input classification, not tile grid generation. Tyre explicitly: "Phase 3 lives entirely in Python tooling and systems.db. It does not add Rust runtime code."
---
## Consensus Points
### 1. Phase 3 is tooling; Phase 5 is runtime — fully parallelizable
All four participants independently frame the same boundary:
- **Phase 3:** Python generator (`generate_regional.py`), offline computation, output committed to systems.db. No Rust runtime code. No game-session dependency.
- **Phase 5:** Rust, seed-derived, computed at runtime when the player enters range of a city. Never stored except as world_seed + ChunkMutations.
The two phases do not block each other during development. Phase 5 develops against a stubbed `CityGenerationContext`; Phase 3 produces the real values; a wiring ticket connects them when both are ready.
- **Gestalt:** "Phase 3 is a tooling phase that enriches systems.db... The Rust server reads it at startup; no Phase 3 computation runs at game time."
- **Tyre:** "Phase 3 lives entirely in Python tooling and systems.db... Phase 3 and Phase 5 are parallel workstreams."
- **Paula:** Treats Phase 3 outputs as stored inputs to Phase 5 throughout.
- **Burnelli-Sheldon:** "These are pre-generation queries, not feedback dependencies."
### 2. `generate_regional.py` is a new generator in the `make regen-db` pipeline
Tyre and Gestalt agree on pipeline order and integration:
```
1. import_economics.py (existing)
2. generate_atlas.py (existing)
3. generate_regional.py (NEW — Phase 3 enrichment)
```
`generate_regional.py` follows the same meta stamp convention as existing generators: writes a stamp to the `meta` table, is listed in `tooling/check-systems-db-stamp`'s `GENERATOR_SOURCES`, and is watched by the `/pr-push` source-file list. Supports `--body`, `--force`, `--dry-run` flags matching `generate_atlas.py` convention.
### 3. `CityGenerationContext` Rust struct is the Phase 3 → Phase 5 handoff
Both Gestalt and Tyre propose the same struct concept. Gestalt's version includes more fields (see Disagreement 1 below). Points of agreement:
- Populated from systems.db at game startup (read-only query path)
- Contains at minimum: `city_id`, `political_archetype`, `prosperity_index`, `surrounding_biome`, `road_entry_directions`, `footprint_radius_km`
- Can be stubbed during Phase 5 development; wired to real data when Phase 3 is complete
### 4. All Round 2 open questions resolved
All seven OQ-R2 items have converged answers from three or more participants in Round 3. See the Open Questions table at the end of this document.
### 5. `prosperity_index` and `perimeter_treatment` belong on DistrictSkeleton before Phase 2
All four participants agree. The cost of inclusion is trivial (derived fields with inputs already available at Stage 1 classification); the cost of retrofit is a Phase 2 rewrite.
- **Tyre:** "Not adding it means Phase 2 tile generation produces identical spatial character regardless of district wealth — then retrofitting it is a Phase 2 rewrite."
- **Burnelli-Sheldon:** "The cost of deferral is not 'decorative refinement later' — it is 'all districts look equally prosperous until a retrofit pass.'"
- **Gestalt:** "Include before Phase 2 is implemented. Retrofit cost > add-now cost."
- **Paula (Round 2):** Original proposal. Not revisited — treated as settled.
### 6. `political_archetype` is a formal derived field with an override slot
All three technical participants converge: formal column on `atlas_cities`, derived from `economic_role` by Python tooling, with a `political_archetype_override` column for named city corrections.
- **Gestalt:** Derives from `economic_role` mapping. Override slot (`political_archetype_override`) allows wiki authors to correct counterintuitive results.
- **Tyre:** Same derivation. `COALESCE(political_archetype_override, <derived>)` at query time.
- **Burnelli-Sheldon:** Derives from `corp_presence` Monopolist share + `governance_type` + `economic_role = transit` + fault-line activity. More granular than Gestalt/Tyre's simple `economic_role` mapping — uses corp behavioral data where available. Starting position: trust the derivation.
Note: Burnelli-Sheldon's derivation is more nuanced than Gestalt/Tyre's direct `economic_role → archetype` mapping. They do not contradict; Burnelli-Sheldon's is a refinement using additional signals.
### 7. Minimum Phase 3 for Phase 5 to proceed: Layers A + B + C
Gestalt explicitly identifies this subset:
> "Layer A: City classification (political_archetype + prosperity_index + founding_orientation). Layer C: Road graph (road_entry_directions per city). Layer B: Regional biome grid (surrounding biome context). Layers D, E, F enrich Phase 5 and the Atlas UI but are not required for the first walkable district."
This gives a sequencing option: ship the minimum Phase 3 subset to unblock Phase 5, then complete the remaining layers in parallel with Phase 5 development.
### 8. Stored-vs-derived boundary extension to Phase 3
Tyre provides the full boundary argument for Phase 3 data belonging in systems.db:
1. Authoring coupling — Phase 3 data derives from wiki-authored bodies data
2. UI dependency — Atlas of the Reach app must display before any game session
3. Generation cost — planet_simulation.py compute time warrants caching at regional resolution
4. Determinism alone does not justify runtime generation
The boundary from Round 2 (atlas markers = last stored layer) extends: Phase 3 data is also stored (committed by `generate_regional.py`). The new inner boundary: DistrictSkeleton and everything below is seed-derived.
---
## GestaltTyre Layer Alignment
Both participants name a Phase 3 layer sequence starting with city classification and ending with wilderness/countryside annotation. The layers map, but not one-to-one. Canonical comparison:
| Canonical | Gestalt label | Tyre label | Content match | Discrepancy |
|-----------|--------------|------------|---------------|-------------|
| Layer A | City Classification + Footprint | Layer A — City Classification | Matched | Tyre omits `founding_orientation` from outputs and CityGenerationContext |
| Layer B | Regional Biome Grid | Layer B — Regional Biome Grid | Full match | None — algorithm, table schema, and Phase 5 consumption are consistent |
| Layer C | Road Network Graph | Layer C — Road Network Graph | Partial match | Tyre's `atlas_road_edges` schema omits `maintenance_authority` column. Gestalt includes it as a Layer C output. |
| Layer D | TerritorialStatus Zones | *(not a distinct layer)* | **Gap** | Tyre has no Layer D equivalent. TerritorialStatus is not in Tyre's CityGenerationContext struct. |
| Layer E | Station Module Topology | Layer D — Station Module Topology | Full match | Same algorithm, same template structure, same tables. Different letter only. |
| Layer F | Wilderness + Settlement Hierarchy | Layer E — Wilderness + Countryside Annotation | Partial match | Tyre's Layer E covers `land_use` annotation only. Gestalt's Layer F adds: `atlas_sub_settlements` table, `PoliticalTether` derivation, `DualNaming` conditions, ruins classification. These are absent from Tyre's schema. |
### CityGenerationContext struct comparison
| Field | Tyre | Gestalt |
|-------|------|---------|
| `city_id: String` | ✓ | ✓ |
| `political_archetype: PoliticalArchetype` | ✓ | ✓ |
| `prosperity_index: f32` | ✓ | ✓ |
| `surrounding_biome: BiomeClass` | ✓ | ✓ |
| `road_entry_directions: Vec<CardinalDirection>` | ✓ | ✓ |
| `footprint_radius_km: f32` | ✓ | ✓ |
| `founding_orientation: FoundingOrientation` | absent | ✓ |
| `surrounding_territorial_status: TerritorialStatus` | absent | ✓ |
| `district_count: u8` | absent (noted in OQ-R2-4 answer) | ✓ |
The three fields Tyre omits correspond directly to: Paula's `FoundingOrientation` proposal (accommodated by Gestalt, not by Tyre), Gestalt's dedicated TerritorialStatus layer (not in Tyre's design), and Tyre's own statement that `district_count: u8` is needed "to prevent a data model migration later."
---
## Paula's Proposals — Coverage by Other Participants
Paula introduced six new fields/concepts in Round 3. Coverage by Gestalt and Tyre/Burnelli-Sheldon:
| Paula Proposal | Gestalt | Tyre | Burnelli-Sheldon | Status |
|---------------|---------|------|-----------------|--------|
| `TerritorialStatus` enum (6 values) | Full — dedicated Layer D with derivation algorithm | Not addressed | Partial — body-level signals support derivation (economic_role, corp_presence); TerritorialStatus not named | Adopted by Gestalt; not resolved by Tyre |
| `FoundingOrientation` enum (5 values) | Full — Layer A output, in CityGenerationContext, drives district layout direction | Not addressed | Not addressed | Adopted by Gestalt only |
| `MaintenanceAuthority` on road segments (5 values) | Full — Layer C output, in `atlas_road_edges` | Absent from `atlas_road_edges` schema | Not addressed | Gestalt adopts; Tyre's Layer C schema gap |
| `PoliticalTether` struct (city_id + TetherRelationship) | Full — Layer F output, `atlas_sub_settlements` table | Not addressed | Dynamic settlements concept is parallel; doesn't use PoliticalTether terminology | Adopted by Gestalt; not in Tyre's schema |
| `DualNaming` / `RegionalFeatureName` | Full — Layer F output, ContestZone trigger condition | Not addressed | Not addressed | Adopted by Gestalt; not in Tyre's design |
| Settlement hierarchy (town/outpost/waypoint/ruin tiers) | Full — `atlas_sub_settlements` table, Layer F | Not addressed | Parallel — "dynamic settlements" (off-atlas mining camps, waypoints, shadow nodes) uses similar placement logic with different framing | Convergent intent; different schema paths |
**Burnelli-Sheldon's "dynamic settlements" vs. Paula/Gestalt's sub-settlement hierarchy:**
The two designs converge on the same need from different angles. Burnelli-Sheldon derives settlement placement from economic trigger conditions (mining camps from corp_presence, waypoints from road length, agricultural nodes from settlement_pattern). Paula/Gestalt derives it from political relationship to nearest city (PoliticalTether). These are not competing designs — Burnelli-Sheldon's trigger conditions provide the *placement logic* while Paula's `PoliticalTether` provides the *relationship classification* once placed. They are compatible layers of the same feature. Neither resolves the schema question (what table, what fields) alone.
---
## Burnelli-Sheldon's Economics Bridge — Key Findings
### The minimum viable read set is 6 fields
Burnelli-Sheldon identifies that a spatially differentiated Phase 1 district skeleton requires exactly 6 systems.db queries per city:
1. `bodies.economic_role` — district type distribution
2. `bodies.settlement_pattern` — district layout and density mode
3. `system_economy.economic_tier` — infrastructure quality and wealth floor
4. `system_economy.distribution_index` — prosperity gradient shape
5. `corp_presence` count at this body — corporate district intensity
6. `corporations.headquarters_system` match — HQ landmark flag
Everything else (behavioral_archetype, shadow economy, bulk_class, brand premiums, currency zone) is Phase 2 enrichment — not required for the minimum walkable world.
### The 10×9 economic_role → DistrictType weight matrix
Burnelli-Sheldon provides a concrete probability weight table mapping all 10 `economic_role` values to all 9 DistrictType values (Residential, Commercial, Industrial, Administrative, LogisticsHub, Entertainment, MixedUse, Transit, Specialized). This is proposed as D-C10 — a locked canonical default from which corp-presence modifiers are applied.
Key examples:
- `manufacturing`: 35% Industrial / 20% LogisticsHub / 25% Residential
- `extraction`: 30% Industrial / 30% LogisticsHub / 20% Residential / 0% Entertainment
- `research`: 35% Specialized / 20% Administrative / 20% Residential / 0% Transit
- `transit`: 25% LogisticsHub / 20% Commercial / 15% MixedUse / 10% Transit
### Three concrete differentiation cases
Burnelli-Sheldon demonstrates how the same DistrictType (Industrial) produces different spatial character depending on economic fields:
- **Tributarium (GJ251c) — Manufacturing Capital:** `bulk_class = precision` → clean-room buildings, not open yards. Gate Corp Monopolist → uniform industrial district with consistent building footprint. Stratified distribution → executive district at prosperity 0.9 adjacent to worker housing at 0.15.
- **Bluebank (GJ3522c) — Extraction Capital:** `bulk_class = bulk` → open pits, conveyor structures, slag heaps. DSMC Monopolist → single employer city, uniform company housing. No Entertainment districts.
- **Strata (GJ280Ad) — Service Mixed Capital:** `bulk_class = non_physical` → office towers, no loading docks. Two reach-wide HQ corps → two landmark buildings. Stratified distribution produces gentrified commercial (0.75) adjacent to mid-tier residential (0.45).
**Burnelli-Sheldon's explicit finding:** "The aesthetic difference is entirely derivable from `production_ubiquity` and `bulk_class`." The generator produces spatial differentiation without explicit authored instructions for each city.
### Economic data quality caveats
Burnelli-Sheldon flags two data quality issues in systems.db that the generator must handle gracefully:
- `behavioral_archetype` on corporations is mostly NULL. Fallback chain: `behavioral_archetype``supply_chain_role` → commodity tier derivation.
- `scope` field on corporations is mixed-type (some text prose, not enum values). Null handling required.
### Simulation feedback requirements (narrow)
Burnelli-Sheldon identifies three fields that genuinely require post-generation reporting back to the economics simulation. All other simulation needs are pre-generation derivable from systems.db alone:
1. **Effective port tile count** — physical district layout determines actual cargo throughput
2. **Corp district allocation** — which DistrictType a corp occupies, post district assignment
3. **Shadow economy footprint** — whether shadow zone generated, and its prosperity floor
---
## Disagreements
### Disagreement 1: TerritorialStatus layer — dedicated layer or absent?
| Participant | Position |
|-------------|----------|
| **Gestalt** | Dedicated Layer D between road graph and wilderness annotation. Derivation algorithm provided: proximity × WorldTier weight + corporate presence + historical tether state. Feeds into CityGenerationContext as `surrounding_territorial_status`. |
| **Tyre** | No dedicated layer. Wilderness annotation (Layer E) classifies land use but does not produce a `TerritorialStatus` enum. `CityGenerationContext` does not include `surrounding_territorial_status`. |
| **Paula** | Proposes `TerritorialStatus` (independent of Gestalt — same enum, same derivation logic). Gestalt accommodates it. Tyre does not address it. |
| **Burnelli-Sheldon** | Does not name `TerritorialStatus` but provides the economic inputs that would support its derivation. No position on the layer structure question. |
The gap is not definitional — all participants who address it agree on the enum values and the derivation logic. The gap is whether `TerritorialStatus` is a Phase 3 stored output (Gestalt's view) or absent from Phase 3 scope (Tyre's implicit omission). This must be decided before Phase 3 schemas are finalized.
### Disagreement 2: Effort estimates (~7d Tyre vs. ~9.5d Gestalt)
| Source | Estimate | Scope difference |
|--------|----------|-----------------|
| **Tyre** | ~7 dev-days | 5 layers: A (1.5d) + B (1.5d) + C (1d) + D/stations (1.5d) + E/wilderness (0.5d) + plumbing (0.5d) + Rust read path (0.5d) |
| **Gestalt** | ~9.5d total / ~5.5d critical path | 6 layers: scaffolding (0.5d) + A (1.5d) + B (1.5d) + C (1d) + D/TerritorialStatus (1d) + E/stations (1.5d) + F/wilderness+settlements (1.5d) + CityGenerationContext (0.5d) + Phase 5 wiring (0.5d) |
Identifiable sources of the ~2.5d gap:
1. Gestalt has a TerritorialStatus layer (1d) absent from Tyre's design
2. Gestalt's wilderness/settlement layer (1.5d) includes sub-settlement placement, PoliticalTether, and DualNaming; Tyre's wilderness annotation (0.5d) is land-use only
3. Gestalt separates CityGenerationContext Rust struct (0.5d) and Phase 5 wiring (0.5d) into distinct tickets; Tyre bundles them as "Rust read path" (0.5d)
If TerritorialStatus, PoliticalTether, and sub-settlements are confirmed in scope, Gestalt's ~9.5d is the correct estimate. If those three are deferred, Tyre's ~7d applies.
### Disagreement 3: `MaintenanceAuthority` in road schema
Gestalt's `atlas_road_edges` schema includes a `maintenance_authority` column. Tyre's `atlas_road_edges` schema does not. Paula proposes `MaintenanceAuthority` as a required field. Tyre does not address this proposal at all. The road graph schema as specified by Tyre cannot support the `MaintenanceAuthority` signal without a schema modification.
### Disagreement 4: Scope of sub-settlement Layer F
Gestalt: Layer F introduces `atlas_sub_settlements` as a new systems.db table with fields per sub-settlement, PoliticalTether derivation, and DualNaming conditions flagged per cell. This is a significant data model addition.
Tyre: Layer E (wilderness) adds only a `land_use` column to `atlas_regional_biomes`. No sub-settlement table. No PoliticalTether. No DualNaming.
Burnelli-Sheldon's "dynamic settlements" placement logic could feed a sub-settlement table without requiring full PoliticalTether classification — but neither participant provides a schema for that middle path.
---
## D-Record Candidates
All candidates from Round 3 source files. Sources attributed.
### D-C11 (proposed by Gestalt and Tyre): Phase 3 layer sequence and tooling architecture
Phase 3 consists of a new generator (`generate_regional.py`) running third in the `make regen-db` pipeline. It enriches systems.db with classification, regional biome data, road graph, station topology, and optionally sub-settlement hierarchy. All Phase 3 computation is offline; no Phase 3 logic runs at game time. `generate_regional.py` follows the same meta stamp and pre-push hook conventions as existing generators.
Minimum confirmed layers: A (City Classification), B (Regional Biome Grid), C (Road Network Graph), D/E (Station Module Topology). Layers for TerritorialStatus and sub-settlements are pending lead decision.
### D-C12 (proposed by Gestalt and Tyre): `CityGenerationContext` as Phase 3 → Phase 5 handoff struct
A Rust struct populated from systems.db at game startup is the handoff point from Phase 3 to Phase 5 district generation. Minimum agreed fields:
```rust
pub struct CityGenerationContext {
pub city_id: String,
pub political_archetype: PoliticalArchetype,
pub prosperity_index: f32,
pub surrounding_biome: BiomeClass,
pub road_entry_directions: Vec<CardinalDirection>,
pub footprint_radius_km: f32,
}
```
Gestalt adds: `founding_orientation: FoundingOrientation`, `surrounding_territorial_status: TerritorialStatus`, `district_count: u8`. These are conditional on the lead decision regarding TerritorialStatus and FoundingOrientation scope.
### D-C13 (proposed by Gestalt): Phase 3 LoD cascade — stored boundary extended
Phase 3 data is stored (committed by `generate_regional.py`). The stored/seed-derived boundary from Round 2 (D-C2) extends: everything at Phase 3 layers (city classification, biome grid, road graph, station topology, optionally sub-settlements) is stored. Everything at Phase 5 (DistrictSkeleton, ChunkData, building footprints) is seed-derived.
### D-C14 (proposed by Tyre): `generate_regional.py` pipeline integration rules
`generate_regional.py` must:
- Write a meta stamp row (generator_name, schema_version, generator_sha)
- Be listed in `tooling/check-systems-db-stamp`'s `GENERATOR_SOURCES` dict
- Be included in the `/pr-push` skill's source-file watch list
- Support `--body <id>`, `--force`, `--dry-run` flags
- Run third (after `import_economics.py` and `generate_atlas.py`) in `make regen-db`
### D-C15 (proposed by Paula and Gestalt): `TerritorialStatus` enum definition
Six-value enum for regional political classification:
`CoreTerritory | FrontierTerritory | ExtractiveZone | ContestZone | WildernessBuffer | AbandonedZone`
Derivation: proximity to cities × WorldTier weight + corporation active presence + historical tether state. WorldTier weights (Gestalt): Epicenter 5×, Regional 3×, Backwater 1×, Passage 1.5×, Waypoint 0.5×.
**Note:** This D-record is proposed by Paula and accommodated by Gestalt. Tyre has not reviewed or accepted it. Requires lead decision before inclusion in Phase 3 schema.
### D-C16 (proposed by Paula and Gestalt): `FoundingOrientation` enum
Five-value enum for city-level derivation:
`PortFacing | RailHeadFacing | ResourceFacing | DefenseFacing | AdminFacing`
Derivation: `settlement_pattern` + `economic_role`. Drives direction of district prosperity gradient and oldest/newest district placement in Phase 5. Stored as a column on `atlas_cities`.
**Note:** Accommodated by Gestalt; not in Tyre's layer design. Requires lead decision.
### D-C17 (proposed by Paula and Gestalt): `MaintenanceAuthority` on road segments
Five-value enum for road edge classification:
`Administrative | Corporate | Communal | Trade | Abandoned`
Derivation: road endpoint types + economics data (same corporation at both ends → Corporate; high trade flow → Trade; no active tether → Abandoned). Stored as a column on `atlas_road_edges`.
**Note:** Absent from Tyre's `atlas_road_edges` schema. Gestalt's Layer C includes it. Requires schema alignment before Layer C is implemented.
### D-C9 (proposed by Burnelli-Sheldon): Economic fields → generator parameter mapping
The spatial generator's minimum read set at generation time: 3 SQL queries (city economic context, system-level modifiers, corporate presence at body). This data is sufficient to derive DistrictType distribution weights, prosperity_index, CorpPresenceTier, political_archetype, perimeter_treatment, and infrastructure quality parameters.
### D-C10 (proposed by Burnelli-Sheldon): `economic_role` → DistrictType weight table
Canonical 10×9 probability weight matrix mapping `economic_role` values to DistrictType values. Proposed as a Rust `const` array keyed by `EconomicRole` enum. Corp-presence modifiers apply on top. Tunable, but locked canonical default prevents per-sprint renegotiation.
---
## Open Questions
### Round 2 questions — now resolved
| ID | Question | Resolution | Resolved by |
|----|----------|-----------|-------------|
| OQ-R2-1 | `prosperity_index` and `perimeter_treatment` in minimum Phase 5 slice? | **YES.** Include before Phase 2. Retrofit cost exceeds add-now cost. | All four |
| OQ-R2-2 | `political_archetype` — formal field or implicit? | **Formal field in `atlas_cities`.** Derived by default. Override column allowed. | Tyre, Gestalt, Burnelli-Sheldon |
| OQ-R2-3 | Phase 2 tile algorithm — door-per-block-edge vs. density-driven rectangles? | **Both, sequenced.** Street skeleton first (door-per-block-edge), then building fill (density-driven rectangles within skeleton). | Tyre, Gestalt |
| OQ-R2-4 | Step 2b (full N-district layout) — minimum slice or follow-on? | **Follow-on.** Data model must support N districts from day 1. `district_count: u8` on CityGenerationContext prevents later migration. | Tyre, Gestalt |
| OQ-R2-5 | Naming register lookup table — before Phase 2 or post-walkable? | **Post-walkable.** Text overlay, not a tile-generation prerequisite. | Tyre, Burnelli-Sheldon |
| OQ-R2-6 | `prosperity_index` for named cities — authored or derived? | **Derived by default.** `prosperity_override REAL` column (NULL = derived) for wiki authors. | Tyre, Gestalt, Burnelli-Sheldon |
| OQ-R2-7 | Naming register table ownership? | Paula taxonomy → Mellanie populates name pools → Tyre reviews spatial constraints → lead locks as D-record. | Tyre |
### New open questions from Round 3
| ID | Question | Source | Priority |
|----|----------|--------|----------|
| OQ-R3-1 | TerritorialStatus as a Phase 3 layer: include in scope (Gestalt/Paula position) or defer beyond Phase 3 minimum (implied by Tyre's omission)? Determines whether Layer D is in scope, whether `surrounding_territorial_status` is in CityGenerationContext, and whether the effort estimate is ~7d or ~9.5d. | Gestalt vs. Tyre | **LEAD DECISION** |
| OQ-R3-2 | `FoundingOrientation`: include in Phase 3 Layer A output and CityGenerationContext (Gestalt/Paula position) or defer (implied by Tyre's omission)? | Paula / Gestalt vs. Tyre | **HIGH** |
| OQ-R3-3 | Sub-settlement hierarchy (Layer F): include `atlas_sub_settlements` table with PoliticalTether in Phase 3 (Gestalt/Paula), or scope Phase 3 to land-use annotation only (Tyre)? | Gestalt/Paula vs. Tyre | **HIGH** |
| OQ-R3-4 | `MaintenanceAuthority` column on `atlas_road_edges`: include in Phase 3 Layer C (Gestalt/Paula position) or absent from schema (Tyre's current design)? This is a concrete schema gap requiring alignment. | Paula / Gestalt vs. Tyre | **HIGH** |
| OQ-R3-5 | Paula OQ-R3-P3 — `DualNaming` / `common_name`: generator output (seeded from territorial conflict conditions) or authored layer only for named locations? | Paula | MEDIUM |
| OQ-R3-6 | Paula OQ-R3-P6 — `TerritorialStatus` zones storage: polygon/voronoi regions in a new table, or tags on the regional biome grid cells? Gestalt implies tagging `atlas_regional_biomes` cells; Paula raises the polygon alternative. | Paula / Gestalt | MEDIUM |
| OQ-R3-7 | Burnelli-Sheldon's `economic_role → DistrictType` weight table (D-C10): proposed as a const Rust array. Is the Round 3 table confirmed as the canonical default, or does it require a tuning pass against real body data before locking? | Burnelli-Sheldon | MEDIUM |
| OQ-R3-8 | Gestalt OQ-R3-G1: Is `corp_presence` granularity body-level sufficient for `TerritorialStatus` ExtractiveZone derivation, or is sub-body geography needed? Burnelli-Sheldon implies body-level is sufficient but does not address sub-body geography explicitly. | Gestalt / Burnelli-Sheldon | MEDIUM |
| OQ-R3-9 | Paula OQ-R3-P5 / Gestalt OQ-R3-G5 — AbandonedZone reclassification: derivable when a corporation's economic presence ends (both participants suggest yes), or does it require an explicit authored flag? Who triggers the reclassification? | Paula / Gestalt / Burnelli-Sheldon | LOW |
---
## Effort Summary
| Scope | Tyre estimate | Gestalt estimate | Difference |
|-------|--------------|-----------------|------------|
| Phase 3 minimum (A + B + C + stations + wilderness) | ~7d | ~7.5d (excl. TerritorialStatus and Layer F richness) | ~0.5d |
| Phase 3 full (as Gestalt / Paula scoped) | — | ~9.5d | ~2.5d gap from TerritorialStatus + rich Layer F |
| Critical path (sequential only) | ~5d (A → C → stations) | ~5.5d (scaffolding → A → C → D → F → struct) | ~0.5d |
| Phase 5 (from Round 2) | ~6d | ~6d | — |
| Phase 3 + Phase 5 minimum total (parallel) | ~7d | ~7.5d | — |
The 2.5d gap is not estimating error — it is a scope question. If OQ-R3-1 through OQ-R3-4 are resolved in favor of inclusion, ~9.5d is correct. If deferred, ~7d applies.
---
## For Lead Decision Before Proceeding
The four highest-priority open questions are lead decisions, not design questions. The design work has been done by the participants; the options are clear:
1. **OQ-R3-1:** Is `TerritorialStatus` in Phase 3 scope? (Determines Layer D, 1d, and `surrounding_territorial_status` in CityGenerationContext)
2. **OQ-R3-2:** Is `FoundingOrientation` in Phase 3 Layer A output? (Adds `founding_orientation` to CityGenerationContext and atlas_cities schema)
3. **OQ-R3-3:** Does Phase 3 include sub-settlement hierarchy? (Adds `atlas_sub_settlements` table and PoliticalTether derivation, ~1d)
4. **OQ-R3-4:** Does `atlas_road_edges` include `maintenance_authority`? (Paula/Gestalt schema vs. Tyre current schema)
All four are architectural choices that shape the Phase 3 schema and the CityGenerationContext struct. They cannot be deferred past the point where `generate_regional.py` scaffolding begins without risking a data model migration.
---
*Notes compiled by Qatux from Round 3 source files. All positions attributed to source participant. No editorial opinions added.*
@@ -0,0 +1,538 @@
---
title: "Round 4 Notes — Generation Cascade Workshop"
description: "Compiled Round 4 outputs: planet-down reframe, two governing rules, layer-by-layer requirements, ghost city effect, latent settlements, and Tyre's brief assembly"
type: workshop
status: active
workshop: generation-cascade
agent: qatux
round: 4
created: 2026-04-30
---
# Generation Cascade Workshop — Round 4 Notes
**Compiled by:** Qatux
**Source files:**
- `docs/workshops/generation-cascade/gestalt-round4.md`
- `docs/workshops/generation-cascade/paula-round4.md`
- `docs/workshops/generation-cascade/burnelli-sheldon-round4.md`
- `docs/workshops/planet-down-cascade/BRIEF.md` (Tyre's assembled brief)
**Scope:** Requirements gathering for the planet-down cascade brief. Round 4 does not design algorithms — it frames what each layer must answer mechanically, narratively, and economically. The output is a new workshop brief, not implementation decisions.
---
## Summary
Round 4 produced a reframe. The workshop entered Round 4 with a Phase 3 map (city classification, biome grid, roads, stations, wilderness) and a Phase 5 scope (district generation, chunk streaming). The lead rejected the city-outward framing and stated the correct ordering: **planet-down**. Civilization is layered onto the natural world. The player enters at the bottom of the stack and reads upward.
Gestalt provided two governing rules that become the architectural spine of all future implementation decisions. Paula provided a narrative rationale for the reframe and resolved the last major Round 3 disagreement (OQ-R3-1: TerritorialStatus is a Layer 2 post-processing output, not a dedicated layer). Burnelli-Sheldon confirmed where economics enters each layer and introduced the latent settlement principle. Tyre assembled all Round 4 material into a new workshop brief (`docs/workshops/planet-down-cascade/BRIEF.md`), converting resolved decisions into Given Facts and surfacing the remaining open questions into structured workshop sections.
Round 3 open questions OQ-R3-1 through OQ-R3-4 were resolved by the planet-down reframe or by participant convergence during Round 4.
---
## Out-of-Scope Flags
1. **No Phase 5 tile generation crept back in.** All four participants maintained the clean Phase 3/Phase 5 boundary from Round 3. Gestalt explicitly notes that the LoD zoom-in cascade is an *architectural description of runtime behavior*, not new Phase 5 scope.
2. **Named city overrides are noted but not designed.** Paula flags that named cities may need `political_archetype_override`, `prosperity_override`, and landmark names. This is deferred to workshop participants in the next brief's sections — the scope is noted, not designed here.
3. **Sub-settlement tier exhaustiveness is not resolved.** The sub-settlement hierarchy (city / town / outpost / waypoint / rural cluster / ruin) is confirmed from Round 3 consensus. How deep each tier goes through district skeleton generation (OQ-R4-G6) is explicitly an open question carried forward.
---
## The Planet-Down Reframe
### Why the reframe
Rounds 1-3 audited an existing implementation (Phase 5 tile generation, chunk streaming, DistrictSkeleton) and then designed Phase 3 to feed it. This was city-outward: start from what exists inside a city and work backwards to what the world needs to provide.
The lead identified this as the wrong causal direction. **The world exists before civilization. Civilization is layered onto it. The player enters at the bottom of that stack and reads upward.** The city-outward audit was not wrong — its findings (DistrictSkeleton, SeedChain, CityGenerationContext, all Round 2 D-records) are valid and carry forward. But the framing that organized those findings is replaced.
### The four-layer model
| Layer | What it is |
|-------|-----------|
| **1. Empty World** | Heightmap → water simulation → sub-biome refinement. Complete natural world, no civilization |
| **2. Population Overlay** | Economics onto geography. Settlements anchor at geographic nexus points. Farmland, roads, sub-settlements |
| **3. City-Level Planning** | Districts, suburbs, industrial zones, ports, stations. The "Cities Skylines" layer |
| **4. Street-Level Rendering** | Streets, buildings, walkable tiles. On demand as the player moves |
Each layer's output is the next layer's input. The layers are causal, not taxonomic.
---
## The Two Governing Rules
Gestalt identifies two rules as the architectural spine. Every system design question reduces to: does this belong in seed-locked layout, or economics-variable rendering?
### DETERMINISM RULE
> Economic simulation's rolling state affects RENDERING (prosperity, repair state) but NOT LAYOUT (streets and buildings locked by seed). A player who leaves a city and returns a year later finds the same streets, richer or poorer.
| Layer | Seed-locked (generated once, never changes) | Economics-variable (runtime updates) |
|-------|--------------------------------------------|------------------------------------|
| **1 — Empty World** | Heightmap, river network, biome grid, pass locations | Nothing — terrain is fixed |
| **2 — Population** | Settlement positions, road topology, sub-settlement placement, PoliticalTether | Road condition (edge case: abandoned roads degrade further) |
| **3 — City Planning** | District positions, district types, block grid, `prosperity_baseline`, `perimeter_treatment` | `prosperity_current` (rolling sim update); building availability |
| **4 — Street-Level** | Street positions, building footprints, door positions, all tile layout | Tile condition states (Intact/Worn/Cracked/Broken); building activity signals |
**The felled-forests exception:** "Felled forests → farmland" is regional land-use (Layer 2, coarse biome-cell resolution), not city-internal tile mutation (Layer 4). Regional land-use CAN update as economics shifts. City-internal street and building geometry is permanently seed-locked.
### LOD/ZOOM RULE
> When a player spawns, the code "zooms in" on the planet — tracing all cascade steps to where the player sits, filling in street-level detail where the player is. Each layer's output is the next layer's input. The cascade is lazy — only what the player needs is computed.
```
Planet in systems.db (Layers 1+2 data committed)
├── Player within planet rendering range?
│ → Regional biome overlay activates for atlas UI
├── Player within city footprint range?
│ → Generate city district grid (Layer 3 / Phase 1)
│ → Build DistrictMap in memory
├── Player within district range (chunk load radius)?
│ → load_chunk() fires per chunk in radius
│ → Layer 4 / Phase 2 generates tile data from BlockSkeleton + chunk_seed
└── Player at building door threshold?
→ Interior descriptor + catalog loaded (door-per-edge)
→ Interior tiles generated (Phase 4+)
```
Cities outside player range have no `DistrictMap` in memory — only their `CityGenerationContext` loaded from systems.db at startup.
---
## Gestalt — Systems Requirements by Layer
### Layer 1: Empty World
What this layer must produce beyond the existing heightmap and 64×32 biome grid:
- **Sub-biome variant tag** per regional cell (3-4 variants per biome class: e.g., `tropical` → jungle_clearing, swamp, riverine_forest, lowland_marsh)
- **`terrain_modification_cost: f32`** per regional cell — how much effort is required to settle here (high cost = forest/hills; low cost = plains/coast)
- **River network** at regional resolution: confluence points, river mouths (these are settlement attractors for Layer 2)
- **Terrain roughness signals** at regional resolution: pass locations through mountain ranges, traversable vs. impassable zones
**Game-mechanical value:** The Empty World is the legibility layer. A player reads terrain to predict settlement density and route quality before seeing a map label. A river confluence with coastal access is almost certainly a city. A mountain range with a single pass is almost certainly a chokepoint. Forced routes, natural shelter, risk gradient — all derivable from terrain before civilization exists.
**Open questions (OQ-R4-G1, OQ-R4-G5):**
- What is stored vs. seed-derived for Empty World data? River confluence points (Layer 2 settlement attractors) must be stored or deterministically queryable before Layer 2 runs. Full river course may be seed-derived.
- At what tile-resolution do rivers appear in walkable generation? If rivers are walkable obstacles (Phase 5), the Phase 2 tile generator needs tile-precision courses. If they're regional navigation constraints only, the 64×32 grid suffices. This is a cross-layer scope question.
**Minimum viable Layer 1:**
- `sub_biome_variant` column on `atlas_regional_biomes`
- `terrain_modification_cost` column on `atlas_regional_biomes`
- River confluence points stored as point features (new `atlas_geographic_features` table)
- Mountain pass candidate cells tagged
- Geographic feature tags (Paula's taxonomy — see below) attached to regional cells
- Defer: tile-resolution river routing, exact lake polygons, authored river vs. generated network reconciliation
### Layer 2: Population Overlay
What this layer produces:
- `land_use` on all regional cells (Urban / Agricultural / Industrial / Wilderness / Corridor / Ruins / Ocean / Impassable)
- Sub-settlement positions (all latent dynamic settlements placed, seed-derived)
- Road graph with `MaintenanceAuthority` per edge
- `TerritorialStatus` per regional cell (post-processing output — see Paula below)
- `political_archetype` per city (derived from geographic trigger + economic data)
- `FoundingOrientation` per city (derived from geographic trigger)
- `PoliticalTether` per sub-city settlement
**Game-mechanical value:** Population Overlay is where the world becomes a problem space. Settlement network topology creates the player's opportunity map. Underserved routes (two settlements, no direct road), extraction hubs (mining camps orbiting a tether city at increasing distance), abandoned corridors (roads leading to ruins) — all generated from absence or presence of infrastructure without authored content.
**Open questions (OQ-R4-G1, OQ-R4-G6):**
- Does "geography determines shape" mean city positions in markers.json are re-evaluated by Layer 2, or stay fixed as anchors with Layer 2 explaining them? The lead's phrasing ("anchored by wiki population counts and markers.json") implies anchored, but this must be confirmed. This determines whether `generate_regional.py` can write back to markers.json or only extends systems.db.
- What is the exact sub-settlement placement algorithm for mining camps, trade waypoints, agricultural nodes?
- Sub-settlement tiers: does a Town (1,000-10,000 population, single-district equivalent) go through the same DistrictSkeleton generation path as a city district, or does it get a simplified code path?
**Minimum viable Layer 2:**
- `land_use` column on all regional cells
- Mining camps + trade waypoints (highest-frequency sub-settlements)
- Road graph with `MaintenanceAuthority` per edge
- `TerritorialStatus` per regional cell
- `political_archetype` per city (derived, override column)
- `FoundingOrientation` per city (derived from geographic trigger)
- Defer: exact agricultural node positions, shadow node placement
### Layer 3: City-Level Planning
What this layer produces:
- N-district grid per city (positions in city-local sim tiles using D-C4 formula)
- `DistrictSkeleton` Phase 1 Stages 1-2 for each district (classification + 4×4 block grid)
- `prosperity_baseline` per district (seed-locked, from WorldTier + economic data)
- `perimeter_treatment` per district
- District spatial arrangement shaped by `FoundingOrientation` + `political_archetype`
- Prosperity gradient direction and magnitude across districts
**Game-mechanical value:** City legibility as player skill. A player who understands political archetypes, WorldTier patterns, and economic roles can walk up to an unfamiliar city and make accurate predictions about its internal structure before entering. CompanyTown = spine pattern; AdminCapital = center-radiating; FreePort = multiple nodes. These are consistent, generatable, and learnable by the player.
**Open questions (OQ-R4-G3, OQ-R4-G4):**
- `prosperity_index` naming: the Round 3 field must be renamed to `prosperity_baseline` (seed-locked Layer 3 output) as distinct from `prosperity_current` (runtime simulation state). These must never be conflated. Tyre's brief implements this naming.
- Does `FoundingOrientation` modify the district placement grid (spatial rotation/alignment), or only modify the prosperity gradient direction without changing grid positions? Explicit spatial orientation produces more legible cities; gradient-only is simpler.
- Does City-Level Planning produce explicit arterial road layout, or are district boundaries the implicit road network?
- Does Layer 3 receive topographic constraints per district, or does it treat all cities as flat for the minimum slice?
**Minimum viable Layer 3:**
- Phase 1 Stage 1: classification per district (DistrictType from weight table, LayoutMode, `prosperity_baseline`, `perimeter_treatment`)
- Phase 1 Stage 2: 4×4 BlockSkeleton grid
- `political_archetype` consumed from systems.db (Layer 2 output)
- Defer: `FoundingOrientation` spatial grid orientation, explicit arterials, topographic constraints per district, Stages 3-5
### Layer 4: Street-Level Rendering
What this layer produces:
- 64×64 `TileEntry` grids (`GeneratorChunkData`) per chunk, on demand
- 3 canonical tile types: `floor_street`, `floor_interior`, `wall`
- Tile condition state per tile: `Intact | Worn | Cracked | Broken` (economics-variable, not seed-locked)
- Building footprints with door positions (door-per-edge boundary)
**Game-mechanical value:** Street level is where the player has physical presence. Seed-locked layout means cities reward familiarity — return visits operate on remembered spatial knowledge rather than re-exploring unfamiliar territory. Economics-variable tile conditions create observable history: a district in decline shows accumulating decay tiles; a developing district shows repair states.
**Open questions (OQ-R4-G2, OQ-R4-G6 partial):**
- How exactly does economics-variable rendering work? Three options: bake at generation + update on event; compute live from `prosperity_current` each frame; cache per chunk, invalidate on threshold crossing. The brief must specify: mechanism, update trigger, caching model.
- What triggers interior tile generation? Pre-generate when player approaches door threshold (avoids stall, wastes compute); generate on door crossing (simplest); pre-generate with exterior (memory cost).
- Does "scatter civilization" include prop spawn points, surface decals, entity spawn points? Layer 4 scope vs. runtime entity system must be defined.
**Minimum viable Layer 4:**
- 3 tile types generating walkable, navigable space
- Seed-locked layout from `district_seed` via FNV-1a
- Chunk streaming hookup: `load_chunk()` calls tile generation via `DistrictMap`
- Defer: tile condition overlays, building interior generation, prop/decal scatter
### Gestalt Open Questions Summary
| ID | Layer | Question | Stakes |
|----|-------|----------|--------|
| OQ-R4-G1 | 2 | City positions in markers.json: anchored with geographic explanation, or re-derived from attractors? | Architecture — determines whether generate_regional.py can write back to markers.json |
| OQ-R4-G2 | 4 | Economics-variable rendering mechanism: bake, live, or threshold-cached? | Implementation — determines tile condition system architecture |
| OQ-R4-G3 | 3+4 | `prosperity_index` naming: `prosperity_baseline` (stored, seed-locked) vs. `prosperity_current` (runtime sim state) — two distinct fields, must not be conflated | Data model |
| OQ-R4-G4 | 3 | Explicit arterial road layout vs. district boundaries as implicit streets | Scope |
| OQ-R4-G5 | 1→4 | River resolution: regional constraint only, or walkable tile-level obstacle? | Cross-layer scope |
| OQ-R4-G6 | 2→3 | Sub-settlement tiers: unified DistrictSkeleton path, or simplified code path for Towns? | Implementation scope |
---
## Paula — "Geography Is Political History"
### The narrative framing
> "Cities don't create geography. Geography creates cities — and then political structures grow to control what geography made valuable."
The planet-down cascade corrects causality for narrative purposes. In the city-outward framing, geographic logic had to be inferred backwards from settlement positions. In the planet-down framing, geographic logic comes first. Settlement character is immediately legible from founding conditions because the generator doesn't need to be told what a pass city is administratively oriented toward — the geography tells it.
### Layer 1: Geographic feature tags
Paula establishes that Layer 1 must produce a **tagged geographic feature layer** for Layer 2 settlement placement to work without re-deriving everything from raw heightmap data.
**Mandatory tags for narrative-legible settlement generation:**
| Tag | What it marks | Political potential |
|-----|--------------|---------------------|
| `RiverConfluence` | Where two navigable rivers meet | Trading nexus; FreePort or AdminCapital archetype |
| `CoastalHarbor` | Natural harbor, protected anchorage | Maritime commerce; FreePort archetype |
| `MountainPass` | Lowest crossing point in a range | Checkpoint/control; AdminCapital or Contested |
| `ArablePlain` | Flat land with soil quality above threshold | Agricultural settlement; dependent on nearest nexus |
| `ResourceConcentration` | Mineral outcrop, energy source, rare material | Extraction; CompanyTown archetype |
| `Defensible` | Elevated with sightlines, natural chokepoint | Military/administrative; AdminCapital |
| `NaturalBarrier` | Range, wide river, dense terrain | Future political boundary |
Layer 1 does not name anything. Rivers, mountains, and passes exist but have no names. Geographic names are first assigned at Layer 2 when settlement begins. Major atlas-visible terrain features may receive geographic placeholder names at Layer 1 for atlas UI labeling.
### Layer 2: TerritorialStatus as post-processing output (resolves OQ-R3-1)
Paula settles the Round 3 disagreement definitively:
> "TerritorialStatus doesn't need its own dedicated layer — it is a post-processing output of the settlement placement and road generation steps that already exist in Layer 2."
The six values are derived after settlement placement completes:
| Value | Derivation |
|-------|-----------|
| `CoreTerritory` | High settlement density + road network well-maintained throughout |
| `FrontierTerritory` | Settlements placed + roads thin at distance from major city |
| `ExtractiveZone` | Settlement at `ResourceConcentration` + corporate presence + excellent extraction corridor + absent off-corridor |
| `ContestZone` | Two settlement clusters with competing road networks in overlapping territory |
| `WildernessBuffer` | No settlement placed — no geographic trigger or economics rationale |
| `AbandonedZone` | Settlement placed (geographic rationale existed at generation time), but economic basis exhausted |
**This resolves OQ-R3-1.** TerritorialStatus is a post-processing classification pass at Layer 2 — it costs one classification step after settlement placement, not a dedicated layer. The Gestalt/Tyre Layer D debate is closed.
### Layer 2: FoundingOrientation is geography, not authoring
`FoundingOrientation` is directly readable from the geographic trigger:
| Layer 2 trigger | FoundingOrientation |
|-----------------|---------------------|
| `CoastalHarbor` or `RiverConfluence` with port | `PortFacing` |
| Transit/rail junction | `RailHeadFacing` |
| `ResourceConcentration` | `ResourceFacing` |
| `Defensible` terrain | `DefenseFacing` |
| No geographic trigger (AdminCapital placed by political decision) | `AdminFacing` |
No authoring pass required. The exception: named cities where setting lore explicitly overrides the geographic logic.
### Layer 2: Road networks and MaintenanceAuthority
`MaintenanceAuthority` falls directly from road endpoint relationships at road generation time — no separate authored pass required:
- Corporate extraction site → corporate depot: `Corporate`
- Administrative capital → administered sub-settlement: `Administrative`
- Settlement cluster with trade flow but no administrative relationship: `Trade`
- Settlement → settlement with no economic or administrative tie: `Communal`
- Connection whose rationale has since dissolved: `Abandoned`
**This resolves OQ-R3-4** — MaintenanceAuthority is derivable at generation time without a separate schema decision beyond adding the column.
### Layer 3: FoundingOrientation shapes spatial direction
`FoundingOrientation` + `political_archetype` determines which direction the city grows and which end is oldest:
- `PortFacing` + `FreePort`: older districts near water, newer development inland
- `ResourceFacing` + `CompanyTown`: city is a spine, not a circle; the resource is at one end
- `DefenseFacing` + `AdminCapital`: city radiates from the fortified high point
- No trigger + `OrganicGrowth`: city grew around multiple centers; no single direction is oldest
The prosperity gradient runs from founding end toward expansion end: oldest districts (closest to founding geography) are most organic, lowest-prosperity; newest development (expansion end) is planned, higher-prosperity. This is a district placement rule, not a separate computation.
### The Ghost City Effect and prosperity_delta
Paula identifies the most powerful narrative consequence of the Determinism Rule:
> "A district's current state and its intended state are always both visible."
**The two-field architecture:**
- **`prosperity_baseline: f32`** — seed-locked Layer 3 output. What the district was planned as. Stored in `CityGenerationContext` and atlas tables.
- **`prosperity_current: f32`** (or `prosperity_delta: f32 = prosperity_current - prosperity_baseline`) — runtime simulation state. What current economic health says.
- **Effective prosperity** = `prosperity_baseline + prosperity_delta` (clamped to [0.0, 1.0]). Drives tile selection.
**The narrative:** When these diverge significantly, the player is looking at the ghost city effect. A CompanyTown where the corporation has departed still has company-town street vocabulary (wide factory roads, tight worker housing grids, no civic center). Streets broadcast what the district was planned to be. Tile decay broadcasts that no one is maintaining that plan. The gap is the story, without a word of dialogue.
This is not a technical refinement. If `prosperity_baseline` is just a sim-updated value with no seed-derived component, the ghost city effect disappears.
### Ruins as structural permanence
When the economic sim flags a settlement for ruination (resource depleted, corporation departed, route obsoleted), the physical structure remains — streets, building footprints, cleared ground. What changes:
- `prosperity_delta` drops sharply
- `perimeter_treatment` degrades (Checkpoint → Fenced → Open)
- Landmark condition deteriorates
The ruin is a ghost city at settlement scale. The geography tagged it as worth settling. Someone built there. Now no one maintains it. The gap between structure (still present) and tile state (decayed) is the full story.
### Accepted proposals remapped to planet-down layers
| Proposal | Origin | Planet-down position |
|----------|--------|---------------------|
| `prosperity_baseline: f32` on DistrictSkeleton | Round 2 (as `prosperity_index`) | **Layer 3 output** (seed-locked baseline); economic sim produces `prosperity_current` at runtime |
| `perimeter_treatment: PerimeterTreatment` | Round 2 | **Layer 3 output** (derived from DistrictType + WorldTier + prosperity_baseline at generation time) |
| `political_archetype` (derived + override) | Round 2 | **Layer 2 output** (derived from geographic trigger + economic data; override slot for named cities) |
| Naming registers (lookup table) | Round 2 | **Layers 2-4**: geographic register at Layer 2, functional/commemorative at Layer 3, street names at Layer 4 |
| Sub-settlement hierarchy | Round 3 | **Layer 2 output** (settlements from geographic triggers; PoliticalTether from why they exist) |
| `MaintenanceAuthority` on roads | Round 3 | **Layer 2 output** (from road endpoint relationships; no separate pass) |
| `TerritorialStatus` | Round 3 | **Layer 2 post-processing output** (after settlement placement completes) |
| `FoundingOrientation` | Round 3 | **Layer 2 → Layer 3 handoff** (directly from geographic trigger) |
| `DualNaming` | Round 3 | **Layer 2 initialization** (ContestZone conditions) → **Layer 3 proliferation****Layer 4 display** |
---
## Burnelli-Sheldon — Economics Integration
### Where economics enters
**Layer 1: Weakly (optional biome probability prior)**
Economics does not shape terrain geometry. But a body's `economic_role` is the *outcome* of someone deciding the terrain was worth exploiting. This creates a plausibility constraint: an `agricultural` world had to have arable soil. Burnelli-Sheldon proposes using `bodies.economic_role` + `bodies.planet_class` as a soft biome probability prior in sub-biome refinement. This is optional — `planet_class` may encode this well enough already. The risk of excluding it is self-contradictory worlds (agricultural role on volcanic terrain).
**Layer 2: In full (primary economics entry point)**
The `atlas_cities`, `bodies`, `corp_presence`, `atlas_roads`, `atlas_railroads`, and `system_economy` tables all enter at Layer 2. Economic role determines hinterland character:
| economic_role | Hinterland fill |
|---|---|
| agricultural | Cleared farmland, irrigation networks, processing nodes |
| extraction | Access corridors, mining infrastructure, stockpile areas |
| manufacturing | Industrial fringe, rail freight yards, supply zones |
| transit | Dense road/rail, relay stations, fuel depots |
| research | Exclusion zones, observatory sites, quiet buffer |
| service_mixed | Suburban residential spread, civic infrastructure |
### The latent settlement principle
Under the Determinism Rule, settlement positions must be seed-locked at generation time. But economic conditions change. The reconciliation:
> The generator places ALL economically plausible settlement positions at Layer 2 generation time. Whether each settlement is ACTIVE is determined by the economic simulation's rolling state. An inactive settlement exists spatially — as ruins, empty structures, cleared ground — but it is dark, unmaintained, and depopulated.
This is economically accurate. Ghost towns exist. The buildings are there; no one lives in them. The road to the closed mine still exists.
**Per settlement type:**
- **Mining camps:** Placed at Layer 2 from `corp_presence` trigger conditions. Active when `corp_financial_state.health_metric > 0.4` for the operating corp. The geological deposit doesn't move when the corp fails.
- **Trade route waypoints:** Long roads (`atlas_roads` with `point_count > 5`) get a seed-derived waypoint at the geometric midpoint. Active when the terminal system remains connected. Even abandoned trade routes leave ruins.
- **Agricultural dispersed nodes:** `settlement_pattern IN ('dispersed', 'dispersed_rural')` AND `economic_role = 'agricultural'` → farm cluster positions at interval derived from `1/economic_tier`. Active by corp health. Fallow farms differ from ghost towns: cleared land, overgrown structures, but clearing and track remain.
- **Shadow economy nodes:** `collection_efficiency < 0.6` → one informal settlement adjacent to the largest atlas city. Active/character changes if enforcement conditions change.
### Prosperity as render parameter, not tile mutation
The most important implication of the Determinism split:
> `prosperity_index` (now `prosperity_baseline`/`prosperity_current`) should NOT be implemented as tile-level ChunkMutations for routine decay. It is a single float per district. The renderer reads it and applies a decay probability to tile selection.
When `prosperity_current` drops from 0.6 to 0.3: streets don't change, building footprints don't change — only the tile SELECTION changes (cracked pavement variant, boarded windows, rust on facades). This happens purely in the rendering path, with no world mutation. **ChunkMutations remain reserved for player-caused or explicit-event damage.**
This distinction matters for performance: prosperity changes don't generate millions of tile mutations.
### Per-layer economic field mappings
**Layer 1:**
| Field | Source | Usage |
|---|---|---|
| `economic_role` | bodies | Biome probability prior (optional) |
| `planet_class`, `atmosphere`, `surface_gravity` | bodies | Primary physical inputs |
**Layer 2 (full list):**
| Field | Source | Usage |
|---|---|---|
| City positions, populations, kinds | atlas_cities | Settlement anchors |
| `economic_role`, `settlement_pattern`, `industrial_corridor`, `population` | bodies | Hinterland character |
| `economic_tier`, `economic_base_primary/secondary` | system_economy | Infrastructure density |
| `gate_connections`, `gate_topology` | system_gates | Trade route intensity |
| `corp_id`, `location_id`, `primary_operation`, `behavioral_archetype` | corp_presence + corporations | Corporate cluster positions |
| Road/rail entries | atlas_roads, atlas_railroads | Existing infrastructure |
| `collection_efficiency` | system_fiscal | Shadow node trigger |
| `health_metric` | corp_financial_state | Settlement active/ghost at generation time |
**Layer 3:**
The 10×9 matrix applies here, modified by topographic constraints from Layer 1 and infrastructure access from Layer 2. Matrix provides zoning prior; terrain provides spatial constraints. Key additions: `distribution_index` sets prosperity gradient magnitude; `FoundingOrientation` (from Layer 2) sets gradient direction.
**Layer 4 at generation time:**
| Field | Source | Usage |
|---|---|---|
| `bulk_class` | commodities via corp_presence | Building archetype |
| `base_premium_multiplier` | brand_products | Landmark building height |
| `prosperity_baseline` | Per-district Layer 3 output | Initial tile variant selection |
**Layer 4 at runtime (no tile mutation):**
| Signal | Source | Render effect |
|---|---|---|
| `prosperity_current` (updated by sim) | Economic simulation | Tile variant selection |
| `corp_financial_state.health_metric` | Economic simulation | Corporate signage presence |
| Active/ghost settlement status | Economic simulation | Lighting, population density rendering |
### Data quality issues (blockers before Layer 3)
1. **`behavioral_archetype` is mostly NULL** on corporation rows. Fallback chain: `behavioral_archetype``supply_chain_role` → commodity tier from `primary_operation`. A backfill step in `import_economics.py` should fill NULL values before `generate_regional.py` runs.
2. **`scope` field is mixed-type text** (enum-ish strings + prose descriptions). Generator cannot reliably parse prose. Until normalized, treat any non-matching value as `system`-tier.
### Burnelli-Sheldon Open Questions
| ID | Question | Recommended answer |
|----|----------|-------------------|
| OQ-R4-B1 | Where does the settlement active/ghost flag live? | In a systems.db Phase 3 output table (settlement positions + trigger conditions). Sim reads these alongside `corp_financial_state`. |
| OQ-R4-B2 | Does "felled forests → farmland" generate ChunkMutations? | No — it's a `regional_land_use` column update at biome-cell resolution, not tile mutation. Different resolution from Layer 4. |
| OQ-R4-B3 | `prosperity_baseline` at district-level or block-level? | Per-district scalar for now (consistent with Phase 5 architecture). Can migrate to BlockSkeleton in a later pass if too coarse. |
| OQ-R4-B4 | How does `distribution_index = stratified` interact with topography? | Topography sets gradient DIRECTION (which end is high-prosperity); `distribution_index` sets gradient MAGNITUDE (how steep). |
---
## Tyre — Planet-Down Cascade Brief Assembly
### What Tyre's brief does
Tyre assembled all Round 4 material into `docs/workshops/planet-down-cascade/BRIEF.md`. This brief is the output of the generation-cascade workshop and the input to the next workshop. It has three components:
1. **The Lead's Cascade Vision** — reproduced verbatim (four layers + two critical rules)
2. **Given Facts** — all decisions from Rounds 1-4 that are locked and must not be revisited
3. **Workshop Sections** — the open questions that the next workshop must resolve
### What became Given Facts
The brief treats the following as locked:
- Phase 3 = Python tooling + systems.db; Phase 5 = Rust runtime + seed-derived; fully parallelizable
- `generate_regional.py` as the third pipeline generator (runs after `generate_atlas.py` in `make regen-db`)
- Atlas markers are the last stored layer; everything from Layer 3 down is seed-derived
- No markers.json extension — city attributes via systems.db cross-reference on `body_id`
- Atlas coordinates are UI-only (no pixel-to-sim-tile formula)
- FNV-1a SeedChain (D-010, D-C6)
- `CityGenerationContext` struct with `prosperity_baseline` field name (replaces `prosperity_index` — implements OQ-R4-G3)
- City decomposition formula (D-C4)
- District Skeleton Phase 1 Stages 1-2 minimum
- WorldTier enum (corrected to Epicenter | Regional | Backwater | Passage | Waypoint)
- `GeneratorChunkData` upgrade to `Vec<TileEntry>` (D-C7)
- 6-field minimum read set (D-C9) and 10×9 weight table (D-C10)
- Sub-settlement hierarchy and placement triggers (Round 3 consensus)
- Latent settlement principle (Burnelli-Sheldon Round 4)
- Determinism boundary table per layer
- LoD zoom-in cascade
### The CityGenerationContext update
The brief's canonical struct resolves OQ-R4-G3 (naming question) and OQ-R3-2 (FoundingOrientation scope):
```rust
pub struct CityGenerationContext {
pub city_id: String,
pub political_archetype: PoliticalArchetype,
pub prosperity_baseline: f32, // seed-locked Layer 3 output — NOT prosperity_index
pub surrounding_biome: BiomeClass,
pub road_entry_directions: Vec<CardinalDirection>,
pub footprint_radius_km: f32,
pub founding_orientation: FoundingOrientation, // now included (OQ-R3-2 resolved)
pub world_tier: WorldTier,
}
```
`prosperity_current: f32` is a separate runtime simulation field. These two must never be conflated in implementation.
### What became open questions in the new brief
The brief surfaces the remaining unresolved questions as structured workshop sections with specific question IDs:
- **L1-Q1 through L1-Q4:** Layer 1 — authored rivers vs. drainage network, resolution, stored vs. derived, biome prior
- **L2-Q1 through L2-Q6:** Layer 2 — anchor vs. re-derive positions, sub-settlement placement algorithm, road generation algorithm, hinterland shapes, TerritorialStatus quantitative thresholds, latent flag storage
- **L3-Q1 through L3-Q7:** Layer 3 — FoundingOrientation spatial effect, political_archetype spatial arrangement, arterial roads, topographic constraints, prosperity_baseline formula, sub-settlement code path, port/station direction
- **L4-Q1 through L4-Q6:** Layer 4 — economics rendering mechanism, condition update trigger, tile algorithm parameters, interior generation trigger, "scatter civilization" scope, prosperity_delta two-field architecture
- **CL-Q1 through CL-Q4:** Cross-layer — prosperity naming confirmation, regional land-use evolution, ruin lifecycle, authored rivers vs. drainage network (elevated)
---
## Round 3 Open Questions Resolved by Round 4
| Round 3 OQ | Question | Resolution | Source |
|----|----------|-----------|--------|
| OQ-R3-1 | TerritorialStatus: dedicated Phase 3 layer or absent? | **Layer 2 post-processing output.** One classification pass after settlement placement + road generation. Not a dedicated layer. | Paula Round 4 |
| OQ-R3-2 | `FoundingOrientation`: include in Phase 3 Layer A + CityGenerationContext? | **Yes.** Directly readable from geographic trigger. In CityGenerationContext. | Paula/Tyre Round 4 |
| OQ-R3-3 | Sub-settlement hierarchy: include `atlas_sub_settlements` table in Phase 3? | **Included as lead inclusion** (confirmed by brief Given Facts). Sub-settlements are part of Layer 2 output. | Tyre's brief |
| OQ-R3-4 | `MaintenanceAuthority` on `atlas_road_edges`: include? | **Yes.** Derivable from road endpoint relationships at generation time. | Paula Round 4 |
---
## Open Questions Carried Forward
These feed directly into the planet-down-cascade workshop brief:
**Must be resolved before Layer 1 can be implemented:**
- OQ-R4-G5 / L1-Q1: Authored rivers vs. seeded drainage network — relationship and reconciliation
- L1-Q2: River network resolution (regional-only or intermediate)
- L1-Q3: Stored vs. derived for river network data
**Must be resolved before Layer 2 can be implemented:**
- OQ-R4-G1 / L2-Q1: City positions — anchored (generate_regional.py explains geography) vs. re-derived from attractors
- L2-Q5: TerritorialStatus quantitative derivation thresholds
- OQ-R4-B1 / L2-Q6: Latent settlement active/ghost flag storage location
**Must be resolved before Layer 3 can be implemented:**
- OQ-R4-G3 / CL-Q1: `prosperity_baseline` vs. `prosperity_current` naming — confirmed in brief; must be enforced in implementation
- L3-Q1: FoundingOrientation spatial grid orientation vs. gradient-only modification
- L3-Q2: `political_archetype` spatial arrangement — explicit vs. emergent
**Must be resolved before Layer 4 can be implemented:**
- OQ-R4-G2 / L4-Q1: Economics-variable rendering mechanism (bake/live/threshold-cached)
- OQ-R4-B2 / CL-Q2: Regional land-use evolution resolution boundary — runtime `regional_land_use` update vs. ChunkMutations
- L4-Q6 / CL-Q1: `prosperity_delta` two-field architecture — formal adoption
---
*Notes compiled by Qatux from Round 4 source files. All positions attributed to source participant. No editorial opinions added.*
@@ -0,0 +1,338 @@
---
title: "Tyre — Round 1: Technical Architecture Inventory"
description: "Layer-by-layer audit of the generation pipeline: state, data format boundaries, dependency order, testing strategy"
type: workshop
status: active
workshop: generation-cascade
agent: tyre
round: 1
created: 2026-04-30
---
# Generation Cascade — Round 1: Technical Architecture Inventory
**Tyre — Technical Architect**
---
## Summary Verdict
The pipeline has a hard cliff at Layer 5. Everything above it (galactic through city placement) is working production code. Everything below it is either stub types or missing entirely. The two-phase district generator architecture is well-designed on paper; zero generation code exists.
The thinnest vertical slice from heightmap to walkable tile — a flat single-district world with no social structure — costs roughly 67 dev-days starting from today's codebase.
---
## Pipeline Inventory
### Layer 1 — Galactic Scale
**State: DONE**
| Property | Detail |
|----------|--------|
| Input | Wiki TOML files, body `index.md` frontmatter |
| Output | `systems.db``star_systems`, `bodies`, economics tables |
| Code | `tooling/economy-db/import_economics.py` + Rust `generate_brands` binary |
| Format | SQLite (file-based; read-only canonical snapshot) |
No gaps. This is the upstream root for all other layers.
---
### Layer 2 — Planetary Heightmap Simulation
**State: DONE**
| Property | Detail |
|----------|--------|
| Input | Body definition dict (planet_class, parameters from `index.md`) |
| Output | Terrain dict: NumPy arrays for elevation, temperature, moisture, surface_water, river_grid |
| Code | `tooling/planet-gen/planet_simulation.py` |
| Format | In-memory NumPy (not persisted separately; consumed immediately by atlas generator) |
No gaps. Called by `generate_atlas.py` as `simulate(body_def)`.
---
### Layer 3 — Atlas City Placement and Infrastructure
**State: DONE**
| Property | Detail |
|----------|--------|
| Input | Terrain dict (Layer 2) + body definition from `systems.db` |
| Output | `markers.json` per body (cities, roads, rail, POIs) + atlas tables in `systems.db` |
| Code | `tooling/planet-gen/generate_atlas.py` |
| Format | JSON files (pixel space: 512×256 grid) + SQLite |
Output schema: `cities[{id, name, kind, center: [row, col], population}]`, roads/railroads as A\* paths. Gate terminal POI placed at largest population centre.
No gaps in the atlas layer itself. The gap is *downstream*: nothing in the Rust runtime consumes this output.
**Critical scale fact:** Atlas coordinates are (row, col) in a 512×256 pixel grid. A district is 512×512 sim tiles = 256×256 visual tiles = 256m. The ratio between atlas pixels and sim tiles is undefined and unimplemented. This is the first missing translation.
---
### Layer 4 — City Naming
**State: DONE**
| Property | Detail |
|----------|--------|
| Input | `markers.json` (empty name fields) |
| Output | `markers.json` (named cities and features, Gemma-driven) |
| Code | `tooling/planet-gen/gemma_naming.py` |
| Format | JSON (same file, in-place update) |
No gaps.
---
### Layer 5 — City-to-District Decomposition
**State: MISSING — no design, no code, no types**
This is the first gap in the entire pipeline. A city marker in the atlas is a single pixel coordinate. A walkable district is 512×512 sim tiles. There is no code or design that answers:
- How many districts does a city of population N get?
- What is the coordinate system mapping from atlas pixel-space to world tile-space?
- How are district boundaries drawn between cities?
- How are inter-city "wilderness" districts handled?
- What district IDs are assigned, and how are they derived deterministically from the world seed?
The `DistrictContext` type on `DistrictSkeleton` is `pub type DistrictContext = String` — it exists to hold world position and neighboring district references, but nothing populates it.
**Data format boundary:** Python JSON (markers.json, 512×256 pixel coordinates) → Rust runtime (`DistrictId`, world tile positions). No bridge code exists.
**Minimum viable implementation:** A function that takes a body's atlas markers and produces a `Vec<(DistrictId, world_tile_origin)>` — one district per city, deterministically positioned. This is the unlock for everything below.
---
### Layer 6 — DistrictSkeleton Generation (Phase 1)
**State: STUB — types compile, zero generation code**
The designed five-stage pipeline from the workshop-outcomes.md:
| Stage | What | Code state |
|-------|------|------------|
| Stage 1: Classification | WorldTier, ComplexityTier, SettingType | Types exist; no generation logic |
| Stage 2: Block grid | 4×4 BlockSkeleton, DistrictLayoutMode | Types exist; no generation logic |
| Stage 3: Reservation | MultiBlockReservation, skyscrapers, terminals | Types exist; no generation logic |
| Stage 4: Social site + NPC | SocialSitePlacement, triangles, DerivedDistrictAnalysis | Types exist; no generation logic |
| Stage 5: Guarantee audit | GuaranteeAuditResult | Type is `String` stub |
Every sub-type that requires actual content or logic is a `pub type Foo = String` alias:
- `DistrictContext`, `SocietyProfileRef`, `ZoneDefinition`, `DistrictBoundaries`
- `GuaranteeAuditResult`, `CorridorSpine`, `ChunkLayout`, `CorridorSpine`
- `Era`, `EraModification`, `LandmarkSlot`, `AccessPoint`
**WorldTier naming mismatch (bug to fix before any generation work):**
`generator.rs` defines: `Peripheral | Connected | Core`
Workshop outcomes (L-3, lead decision): `Epicenter | Regional | Backwater | Passage | Waypoint`
These are incompatible. The workshop canonical values must replace the current enum before the generation spike begins.
**Missing DistrictSkeleton fields:**
The struct in `generator.rs` is missing three fields specified in the workshop outcomes design:
- `vertical_structure: VerticalStructure` (from D-READY-11)
- `breach_only_zones: Vec<ZoneId>` (from guarantee audit, Tier 2)
- `derived_analysis: DerivedDistrictAnalysis` (Phase 1 computed, from Miri/Gestalt)
**Minimum viable implementation:** Stage 1 + Stage 2 only. Classification from WorldTier + flat 4×4 block grid with uniform zoning. No reservations, no social sites, no audit. Produces a compilable DistrictSkeleton that can be handed to Phase 2.
Workshop estimate for Phase 1 basic: ~3 dev-days.
---
### Layer 7 — ChunkData Generation (Phase 2)
**State: MISSING — no code, no type resolution**
Phase 2 is the per-chunk fill step that runs when a chunk is first loaded. It reads the `BlockSkeleton` for the block containing the chunk and produces tile data (walls, floors, furniture placements).
`GeneratorChunkData` is `pub type GeneratorChunkData = Vec<bool>` — a flat boolean walkability array. The actual tile type system (`TileId`) is `pub type TileId = String`. No tile vocabulary exists.
The heritage grammar overlay system (D-READY-9, `HeritageGrammarOverlay`) does not exist as Rust code or TOML data. It was designed in the workshop but nothing has been authored.
**Data format boundary:** `BlockSkeleton` (Rust struct) → 64×64 tile grid (`GeneratorChunkData`). The generation logic for this conversion is entirely absent.
**Minimum viable implementation:** Produce a flat walkable floor tile grid for each chunk, with wall tiles on chunk boundaries. No rooms, no furniture. Just a walkable district footprint. Heritage grammar can be deferred.
Workshop estimate for Phase 2 basic: ~4 dev-days. With heritage grammar: substantially more.
---
### Layer 8 — Chunk Streaming + Generator Integration
**State: PARTIAL — streaming works, generator hookup is zero**
`chunk_streaming.rs` correctly manages load/unload based on player position. It compiles, passes tests, and handles z-level isolation properly. The architecture is sound.
The gap: `WalkabilityMap::load_chunk()` creates `ChunkData::new_walkable()` — a fully-walkable blank placeholder. It has no knowledge of districts, no connection to any generator, and no way to distinguish "in a building" from "in open space."
For Phase 5 (generated world), `load_chunk()` needs to:
1. Determine which district the chunk coordinate falls within
2. Look up the `DistrictSkeleton` for that district
3. Identify the `BlockSkeleton` for the block containing this chunk
4. Run Phase 2 fill and return real tile data
None of steps 14 exist. The streaming system is a ready container waiting for content.
**Minimum viable integration:** A `DistrictMap` resource mapping `ChunkCoord → DistrictId`, and a Phase 2 dispatch in `load_chunk()`. ~1 dev-day once Phase 1 and 2 exist.
---
### Layer 9 — Social Site Templates and Triangle System
**State: DONE (runtime system, not wired to generator output)**
`triangle.rs` is fully implemented production code:
- `FullTemplateDef`, `RoleSchema`, `SpaceSpec`, `TriangleDef` — content schema with validation
- `generate_intra_template_triangles` and `generate_cross_template_triangles` — working
- `tick_triangle_escalation` and `apply_resolve_triangle` — working ECS systems
- Tests pass, serialization correct, FNV-1a determinism enforced (D-010)
The gap: `DistrictSkeleton.social_sites` contains `Vec<SocialSitePlacement>` with `template_tag: String` references. Nothing reads those tags and spawns `TemplateOwnership` components. The link from generator output to runtime triangle population is missing.
---
### Layer 10 — NPC Generation Spike
**State: PARTIAL — proof-of-concept, not production pipeline**
`server/src/bin/generator_spike.rs` (Sprint 25) generates NPCs from `ZoneSpec + CultureProfile` inputs. It produces named NPCs with traits, wants, behaviors, relationships, and want-tells. It works and demonstrates the 10-axis model is viable.
However: it is a standalone binary that bypasses ECS entirely. It operates on `ZoneSpec` (not `DistrictSkeleton`). It is not wired to the spatial generation pipeline. Its output is printed to stdout, not spawned into the ECS world.
The gap between this spike and production NPC generation: `ZoneSpec` must be derived from `BlockSkeleton.zoning` + `DistrictSkeleton.society_profile`, then NPCs must be spawned with `TemplateOwnership` components into the bevy ECS world.
---
## Data Format Boundaries
Four boundaries, three of them currently bridgeless:
```
Python atlas output → [BRIDGELESS] → Rust runtime
markers.json → → DistrictMap resource
(512×256 pixel coords) → → (sim tile coords)
Atlas city marker → [BRIDGELESS] → District grid
(row, col) pixel → → Vec<(DistrictId, ChunkCoord)>
DistrictSkeleton (Rust) → [BRIDGELESS] → ChunkData (Rust)
Phase 1 output → → Phase 2 fill
BlockSkeleton → → 64×64 tile grid
chunk_streaming.rs → [PARTIAL] → generator
load_chunk() → → ChunkData::new_walkable() (wrong)
```
The only working boundary is Python atlas → `systems.db` (the atlas tables), which is consumed by the Godot atlas map UI but not by the game simulation server.
---
## Strict Dependency Order
```
[DONE] Layer 1-4: Atlas pipeline complete
[MISSING] Layer 5: City-to-district decomposition ← FIRST UNLOCK
[STUB] Layer 6: DistrictSkeleton Phase 1 generation
↓ (WorldTier enum fix is a prerequisite)
[MISSING] Layer 7: ChunkData Phase 2 fill
[PARTIAL] Layer 8: chunk_streaming → generator wiring
↓ character/apartment work gates here
[BACKLOG] #681 Apartment generator
[BACKLOG] #682 Apartment rendering
```
**Parallelization opportunities within the blocked stack:**
- Once Layer 5 design is locked, Phase 1 (Layer 6) and content authoring (heritage grammar TOML, zone-type RON files, culture profiles) can proceed in parallel — they share no code dependency
- Different districts can be generated in parallel once Layer 6 code exists (each district is seed-isolated)
- Triangle template YAML authoring has no code dependency on Layer 5-8 and can proceed now
**NOT blocked by generation pipeline:**
- #619 Full character customization — Phase 4 work on a hand-authored 2-floor test map
- #694 Character creation screen — Phase 4, same rationale
- #616 Verb vocabulary design — content design work, cascade phase 4
---
## Ticket Analysis
### Tickets with incorrect cascade position (should be formally blocked)
| Ticket | Title | Current status | Issue |
|--------|-------|----------------|-------|
| #681 | Apartment generator (server) | backlog | Needs walkable generated world (Layers 5-8 complete). Should be explicitly blocked. |
| #682 | Apartment rendering (client) | backlog | Blocked by #681, which is blocked by the generation pipeline. |
| #615 | Tycoon small business starting state | backlog | Feeds #681. Should be blocked behind pipeline completion. |
### Tickets that can proceed (Phase 4 per cascade definition)
| Ticket | Title | Reasoning |
|--------|-------|-----------|
| #694 | Character creation screen | Phase 4: player control scheme on 2-floor test map. Does not require generated world. |
| #619 | Full character customization | Phase 4: same. Aesthetic-only, works on hand-authored map. |
| #616 | Verb vocabulary design | Content design. No code dependency on generation pipeline. |
### New tickets needed
These represent the actual missing implementation work:
1. **"Fix WorldTier enum values in generator.rs"** — Replace `Peripheral/Connected/Core` with `Epicenter/Regional/Backwater/Passage/Waypoint` per lead decision L-3. Blocked by nothing. ~0.5 dev-days.
2. **"City-to-district decomposition — spec and implementation"** — Define atlas pixel → world tile coordinate mapping, district count per city, district boundary algorithm. Produces `Vec<(DistrictId, world_tile_origin)>` from atlas markers. Blocks all Phase 1/2 work. ~2 dev-days.
3. **"DistrictSkeleton Phase 1 generator (Stages 1-2)"** — Classification + block grid. Minimal viable Phase 1 that produces a compilable skeleton. Blocked by ticket 1 and 2. ~3 dev-days.
4. **"Add missing DistrictSkeleton fields"** — `vertical_structure`, `breach_only_zones`, `derived_analysis` from workshop spec. Can be stub types initially, but must be present for schema completeness. ~0.5 dev-days.
5. **"ChunkData Phase 2 minimal fill"** — Flat walkable floor with wall boundaries. No rooms, no furniture. Blocked by ticket 3. ~2 dev-days (minimal slice).
6. **"Wire generator to chunk_streaming"** — `DistrictMap` resource + dispatch in `load_chunk()`. Blocked by tickets 2, 3, 5. ~1 dev-day.
---
## Testing Strategy Per Layer
**Design principle:** Each layer produces deterministic output from a seed. Same seed = same output. This is the primary invariant to test at every layer.
| Layer | Test approach | Independent? |
|-------|--------------|--------------|
| Atlas (done) | `make regen-db` + `make check-systems-db-stamp` + visual Godot inspection | Yes |
| City-to-district decomp | Unit test: given N markers, produces M districts with valid positions and no overlaps. Determinism: same seed → same output. | Yes |
| DistrictSkeleton | Unit test: same seed → identical skeleton (byte-level). Coverage test: 100 seeds → no two identical skeletons. Guarantee audit: all Tier 1 sites present for inhabited districts. | Yes |
| ChunkData fill | Gauntlet snapshot test (new Gauntlet room for generated district). Visual inspection in `--test-mode SR_LIVE=1`. Walkability consistency: every Phase 2 chunk must have ≥1 walkable tile. | Yes |
| Chunk streaming integration | Existing `chunk_streaming` tests verify load/unload logic. New: integration test verifying loaded chunks are non-blank after generator wiring. | Partially (streaming logic tested; fill content not yet) |
The Gauntlet test world should get a dedicated "generated district" room for Phase 2 validation once Phase 1+2 exist. Per CLAUDE.md rules, this must be a new room, not modification of existing rooms.
---
## Implementation Priority Order
Ordered by what unblocks the most:
1. **Fix WorldTier enum** — 0.5 days, unblocks all generator work (nothing runs correctly until this is right)
2. **City-to-district decomp** — 2 days, the first real gap; unblocks all Phase 1/2 code
3. **DistrictSkeleton Phase 1 (Stages 1-2 only)** — 3 days, produces first generated skeletons
4. **Missing DistrictSkeleton fields** — 0.5 days, schema completeness
5. **ChunkData Phase 2 minimal** — 2 days, produces first walkable generated tiles
6. **Chunk streaming integration** — 1 day, first end-to-end walkable generated world
7. **Full Phase 2 with rooms/furniture** — 4+ days (after vertical slice validated)
8. **Vertical structure / z-levels** — 7 days, Phase 4/5+ (per workshop estimate)
9. **MobileChunk** — 9.5 days, deferred until static districts work
**Total for thinnest vertical slice (flat walkable district):** ~9 dev-days
**Formal gate:** Character and apartment work (#681, #682, #615) should be blocked in the ticket system behind the vertical slice milestone (items 1-6 above complete).
---
## Appendix: Key Architectural Constraints to Preserve
These are non-negotiable from the workshop and D-records:
- **D-010 determinism:** All generation must consume randomness exclusively through `SimRng`. No `std::hash::DefaultHasher`. Same seed = same world, always.
- **D-010 BTreeMap:** All `HashMap` usage in generator output types is prohibited. Use `BTreeMap` throughout.
- **D-108 MobileChunk:** Vessel interiors use the same Phase 2 primitives as static districts. No separate generation path.
- **D-110 signed z-levels:** `base_z: i8` for positions, `z_levels: u8` for counts. Already implemented correctly in the types.
- **L-7 XOR prohibition:** In-playthrough events use `DamageOverlay` exclusively. No XOR reseeding.
- **Two-phase immutability:** Phase 1 output is frozen. All runtime changes go through the overlay/delta layer.
- **chunk_streaming cadence:** Default 10 ticks (1 game-minute). Phase 2 fill must complete within a single cadence window or be deferred to background.
@@ -0,0 +1,488 @@
---
title: "Tyre — Round 2: Spatial Generation Architecture Proposal"
description: "Technical architecture for the spatial pipeline: coordinate translation, city-to-district decomposition, Phase 1/2 generation, chunk wiring, seed boundary, testing, and effort"
type: workshop
status: active
workshop: generation-cascade
agent: tyre
round: 2
created: 2026-04-30
---
# Generation Cascade — Round 2: Spatial Generation Architecture Proposal
**Tyre — Technical Architect**
---
## Scope Declaration
Per lead directive: this proposal covers the **spatial pipeline only** — from atlas city markers to walkable rendered tiles. Heritage grammar, NPC manifests, social sites, triangle wiring, culture overlays — all deferred. The remaining questions from Round 1 that fall outside spatial scope (OQ-R1-A heritage root count, OQ-R1-B SocietyProfileRef type) are not addressed here.
Spatial scope items I am addressing:
- OQ-R1-C: City marker schema approach
- OQ-R1-D: Atlas coordinate translation formula (resolved below)
- OQ-R1-E: Vertical slice effort estimate (revised)
- Gap B: Coordinate system design
- Gap C: Missing DistrictSkeleton fields
---
## The Core Architecture Insight: Two Separate Coordinate Systems
This resolves OQ-R1-D and underpins everything below.
The atlas grid (512×256 pixels) represents the **planetary surface** as a display coordinate system for the atlas UI. It shows continents, coastlines, city dots. It is not a world-space coordinate for the game simulation.
The walkable world uses a **city-local coordinate system**, measured in sim tiles (0.5m each, per D-066). A city does not "exist" at pixel (row, col) in the sim world — it exists in its own local space starting at (0, 0). The atlas pixel position is only used for:
1. Rendering the city dot on the planetary map UI
2. Determining relative positions *between* cities on the same body (future: overland travel)
**There is no pixel-to-sim-tile conversion formula** and none is needed. The translation is a lookup, not arithmetic:
```
atlas_city.center = [row, col] → planetary UI display position only
walkable world origin → always (0, 0) in city-local sim tile space
```
This is the correct architectural answer to OQ-R1-D. Attempting to produce a linear formula from 512×256 pixels to a planetary sim-tile grid would require deciding how large the planet is in sim tiles — a meaningless number for gameplay. Players don't walk between cities.
**What this means for district positioning:** Districts in a city are laid out in city-local sim tile space. District 0 is at world position (0, 0). District 1 is at (512, 0) (one district-width east). And so on. The atlas pixel coord stays in the atlas layer; the district layout algorithm works purely in sim tile space.
---
## Seed vs. Stored-Data Boundary
This is the most important architectural decision in the spatial pipeline. I'll be direct about where the line is.
### What is stored (authored or generated-then-committed)
| Data | Location | Why stored |
|------|----------|-----------|
| Star system and body definitions | `wiki/`, `systems.db` bodies table | Authored world content |
| Economic data, corporations | `systems.db` economics tables | Generated-then-committed via pipeline |
| City positions, populations, infrastructure | `markers.json`, `systems.db` atlas tables | Generated-then-committed; hand-edit possible |
| City names | `markers.json` (Gemma-filled) | Curated; not reproducible on demand |
| Chunk mutations (DamageOverlay, player changes) | Save file (future) | Playthrough-specific |
### What is NOT stored — always re-derived from seed
| Data | Why seed-derived |
|------|-----------------|
| District count and positions per city | Function of (city_data, world_seed) |
| DistrictSkeleton (all fields) | Function of (DistrictId, body_context, world_seed) |
| BlockSkeleton per district | Function of (district_seed) |
| ChunkData tile grid | Function of (block_skeleton, chunk_coord, district_seed) |
| Building footprints within chunks | Function of (zoning, density, chunk_seed) |
**The boundary is between Layer 4 and Layer 6:** everything from city markers downward is seed-derived and never persisted as generator output. The `markers.json` city entries are the stored anchor. Below that, the world is fully deterministic from `world_seed`.
### The SeedChain
Every child seed is derived from its parent using FNV-1a (per D-010's determinism mandate):
```rust
fn child_seed(parent: u64, discriminant: u64) -> u64 {
let mut h = parent ^ 0xcbf29ce484222325;
for byte in discriminant.to_le_bytes() {
h ^= byte as u64;
h = h.wrapping_mul(0x100000001b3);
}
h
}
district_seed = child_seed(world_seed, district_id)
chunk_seed = child_seed(district_seed, (cx as u64) << 32 | (cy as u64))
```
This means:
- Any district can be generated in isolation, without generating its neighbors first
- Any chunk can be generated in isolation, given its district_seed
- Generation is embarrassingly parallelizable
- No global state is required
### Implication for save files
Save files store:
- `world_seed: u64` (the single master seed)
- `markers_snapshot: HashMap<BodyId, MarkersJson>` (or a hash of it for validation)
- `chunk_mutations: BTreeMap<ChunkCoord, ChunkMutations>` (only player-modified chunks)
Everything else is re-derived on load. A save file for a large world stays small.
---
## Layer-by-Layer Architecture Proposal
### Layer 6: City → District Decomposition
**Inputs:**
- `body_id: &str`
- `city: &AtlasCity` (from systems.db atlas_cities table — `id`, `kind`, `population`)
- `body: &Body` (from systems.db bodies table — `settlement_pattern`, `economic_role`, `planet_class`)
- `world_seed: u64`
**Algorithm:**
```rust
fn decompose_city(city: &AtlasCity, body: &Body, world_seed: u64) -> Vec<DistrictPlacement> {
let n = district_count(city.population, city.kind, body.settlement_pattern);
let layout = city_layout_grid(n, city_seed(world_seed, &city.id));
layout.iter().enumerate().map(|(i, grid_pos)| DistrictPlacement {
district_id: district_id(world_seed, &body.body_id, &city.id, i as u32),
world_origin: grid_to_sim_tiles(grid_pos), // (col * 512, row * 512) in sim tiles
city_id: city.id.clone(),
}).collect()
}
```
**District count formula:**
```
population → n_districts:
0 → 0 (uninhabited)
150k → 1
50k500k → 24 (log-scaled)
500k5M → 49 (log-scaled)
5M+ → 916 (capped at 4×4)
capital → +1 bonus district
domed/cave → fixed 1 regardless of population
```
**District positioning:** A simple grid arrangement in city-local space. District at grid position (col, row) gets world origin `(col * 512, row * 512)` sim tiles. Grid dimensions chosen to be roughly square for the given count (e.g., 4 districts → 2×2; 6 districts → 3×2). Exact grid shape is seeded for slight variety (some cities are long and thin, some square).
**OQ-R1-C resolution:** `primary_function`, `planet_class`, and `settlement_pattern` are available in `systems.db` bodies table and do NOT need to be added to `markers.json`. The decomposition function reads them from systems.db directly, cross-referenced by body_id. `generate_atlas.py` does not need modification.
**Output:**
```rust
struct DistrictPlacement {
district_id: DistrictId, // u64, deterministic from world_seed + body + city + index
world_origin: (i64, i64), // sim tile position of district's (0,0) corner
city_id: String,
}
```
**Testing:** Pure function. Unit test: given (population, settlement_pattern), district count matches formula. Given same (city, body, seed), district positions are identical across runs. Given different seeds, positions vary. No ECS needed.
---
### Layer 7: Phase 1 — DistrictSkeleton (Spatial Structure Only)
**Scope for minimum vertical slice:** Stages 1 and 2 only. Stages 3 (reservations), 4 (social sites), and 5 (guarantee audit) are deferred — they produce spatial enrichment but are not required for a walkable tile.
**Stage 1: Classification**
```rust
fn classify_district(placement: &DistrictPlacement, body: &Body, city: &AtlasCity)
-> (WorldTier, ComplexityTier, SettingType, DistrictType)
```
- `WorldTier`: derived from system economic tier in systems.db (`economic_role`, system connectivity). Hub systems → `Epicenter`; standard → `Regional` or `Backwater`; transit stops → `Passage`.
- `ComplexityTier`: derived from WorldTier ceiling table (workshop-outcomes.md)
- `SettingType`: derived from `planet_class` + `economic_role` + city `kind`. Capital on a standard planet → `Urban`. Rural body → `Agricultural`. Orbital station → `Station`.
- `DistrictType`: derived from city `kind` + district index within city (center district → `Commercial` or `MixedUse`; outer districts → `Residential`, `Industrial`, `Transit`)
All deterministic from upstream data. No RNG needed at Stage 1 — classification is a pure lookup/mapping.
**Stage 2: Block grid**
```rust
fn generate_block_grid(district_seed: u64, district_type: &DistrictType,
complexity: &ComplexityTier) -> [[BlockSkeleton; 4]; 4]
```
- `DistrictLayoutMode`: seeded choice between `Grid` and `Organic`. Weight toward Grid for Commercial/Administrative, toward Organic for Residential/Mixed. Lead decision L-2: both modes must coexist.
- Per block: assign `ZoningType` from a seeded draw against the district's type-to-zoning probability table. Commercial district → mostly Commercial blocks with some Residential edges.
- Per block: assign `density_pct` seeded from district_seed + block position. Creates density gradient (center blocks denser than edge blocks for Urban districts).
- `Era`: stub string for now (`"standard"`). Real era differentiation is later work.
- Everything else on `BlockSkeleton` (`chunk_layout`, `hosted_sites`, `landmark`) → left as empty/stub.
**Key constraint:** The block grid is fully determined by `(district_seed, district_type, complexity)`. No external lookups at generation time. The entire `DistrictSkeleton` can be re-derived from those three inputs plus `world_seed`.
**Missing fields to add:** Per Gap C from Round 1, these three fields are required by the workshop spec and absent from the current struct:
- `vertical_structure: VerticalStructure` — add as stub enum `VerticalStructure::Flat` for now
- `breach_only_zones: Vec<ZoneId>` — add as `Vec<String>`, empty for minimum slice
- `derived_analysis: DerivedDistrictAnalysis` — add as stub struct, all fields `None`/default
These are 0.5 days of type work.
**WorldTier fix:** The enum must be corrected before any generation work starts. One-line change, 0.5 days including test update.
---
### Layer 8: Phase 2 — ChunkData Tile Generation (Spatial Only)
This layer converts a `BlockSkeleton` into a 64×64 sim-tile grid of walkable/non-walkable tiles with tile type assignments.
**Inputs:**
- `block: &BlockSkeleton` (zoning, density_pct, layout from Phase 1)
- `chunk_coord: ChunkCoord` (which 2×2 chunk-within-block this is)
- `chunk_seed: u64` (derived from district_seed + chunk_coord)
**Output:** A 64×64 tile grid (the raw `GeneratorChunkData`)
**Minimum viable generation algorithm:**
The key insight: for the minimum slice, we don't need heritage grammar, room templates, or furniture. We need:
1. Street tiles (walkable, visually distinct)
2. Building exterior tiles (wall, non-walkable)
3. Building interior floor tiles (walkable)
```rust
fn generate_chunk_tiles(block: &BlockSkeleton, within_block_pos: (u8, u8),
chunk_seed: u64) -> GeneratorChunkData {
let density = block.density_pct;
let mut rng = SimRng::from_seed(chunk_seed);
// Step 1: Fill with street tile
// Step 2: Place building footprints based on density
// - density 0-20%: 0-1 small buildings
// - density 20-60%: 2-4 medium buildings
// - density 60-100%: 4-8 buildings, larger, tighter
// Step 3: For each footprint: fill interior with floor tile, perimeter with wall tile
// Step 4: Ensure street connectivity (no building can fully block a chunk edge)
}
```
**Building footprint algorithm:** Simple non-overlapping rectangle placement using seeded random dimensions within density budget. Minimum building: 6×6 tiles. Minimum street gap between buildings: 4 tiles. This is not artistically sophisticated but produces walkable, navigable space.
**Tile vocabulary for minimum slice:**
```rust
// Minimum TileId set (just string constants to start)
const TILE_FLOOR_STREET: &str = "floor_street";
const TILE_FLOOR_INTERIOR: &str = "floor_interior";
const TILE_WALL: &str = "wall";
```
Three tile types. Everything needed for walkability and basic navigation. Visual polish is a later pass.
**`GeneratorChunkData` type change:** The current type alias `pub type GeneratorChunkData = Vec<bool>` must become a proper struct to carry tile IDs, not just walkability. Minimum:
```rust
pub struct GeneratorChunkData {
pub tiles: Vec<TileEntry>, // 64×64 = 4096 entries
}
pub struct TileEntry {
pub tile_id: TileId, // "floor_street", "wall", etc.
pub walkable: bool, // pre-computed for the movement system
}
```
This is a type-level change that breaks the placeholder `ChunkData::new_walkable()` usage cleanly — making the transition visible rather than silent.
---
### Layer 9: Chunk Streaming → Generator Wiring
The chunk streaming architecture is ready. The hookup requires two additions:
**Addition 1: `DistrictMap` resource**
```rust
#[derive(Resource)]
pub struct DistrictMap {
/// Maps a chunk coord to the district it belongs to.
/// Built at world load time from DistrictPlacement data.
chunks: BTreeMap<ChunkCoord, DistrictId>,
/// The Phase 1 skeleton for each loaded district.
skeletons: BTreeMap<DistrictId, DistrictSkeleton>,
}
```
**Addition 2: Replace `load_chunk()` placeholder**
```rust
pub fn load_chunk(&mut self, coord: ChunkCoord, district_map: &DistrictMap,
world_seed: u64) -> bool {
if self.chunks.contains_key(&coord) { return false; }
let chunk_data = if let Some(district_id) = district_map.chunks.get(&coord) {
let skeleton = &district_map.skeletons[district_id];
let block = block_for_chunk(skeleton, coord);
let chunk_seed = child_seed(child_seed(world_seed, *district_id),
coord_to_u64(coord));
generate_chunk_tiles(block, within_block_pos(coord), chunk_seed)
} else {
// Outside any district — wilderness, open terrain
GeneratorChunkData::all_walkable()
};
self.chunks.insert(coord, chunk_data);
true
}
```
This is the complete wiring. No other changes to `chunk_streaming.rs` are needed.
---
## Data Format Boundaries
The handoff between Python tooling and Rust runtime is clean and already exists:
```
PYTHON TOOLING (offline pipeline)
systems.db → bodies, star_systems, economics, atlas_cities tables
markers.json → city positions and populations (per body)
↓ [HANDOFF — server startup reads these two sources]
RUST SERVER (runtime)
At startup: read systems.db + markers.json → build DistrictMap in memory
On load_chunk(): derive ChunkData from district_seed + block_skeleton
On save: persist only ChunkMutations (player-modified chunks)
```
**The handoff is currently wired:** `server/src/main.rs` references the generator. Systems.db is read at startup. The gap is not the boundary mechanism — it's that no code reads atlas_cities to build a DistrictMap.
**No Python layer needed for Phase 1 or Phase 2.** The entire spatial generation below city markers is Rust code, working against data already in systems.db. This is clean.
---
## Testing Strategy Per Layer
Each layer produces deterministic output and can be validated independently.
### City decomposition
```rust
#[test]
fn decompose_city_deterministic() {
let result1 = decompose_city(&city, &body, 42);
let result2 = decompose_city(&city, &body, 42);
assert_eq!(result1, result2);
}
#[test]
fn district_count_formula() {
assert_eq!(district_count(0, "city", "standard"), 0);
assert_eq!(district_count(10_000, "city", "standard"), 1);
assert_eq!(district_count(1_000_000, "capital", "standard"), 5); // 4 + 1 capital bonus
}
```
Fully unit-testable. No ECS, no file I/O.
### Phase 1 DistrictSkeleton
```rust
#[test]
fn skeleton_same_seed_identical() {
let s1 = generate_skeleton(placement, body, city, 42);
let s2 = generate_skeleton(placement, body, city, 42);
assert_eq!(s1, s2); // requires PartialEq on DistrictSkeleton — needs real types, not String stubs
}
#[test]
fn block_grid_coverage() {
let s = generate_skeleton(placement, body, city, 42);
// Every block has a valid ZoningType
for row in &s.blocks {
for block in row {
assert!(block.zoning != ZoningType::Mixed || s.district_type == DistrictType::MixedUse);
}
}
}
```
Requires replacing String stub types with real enums/structs (which is also needed to implement generation). Tests become the specification.
### Phase 2 ChunkData
```rust
#[test]
fn chunk_has_walkable_tiles() {
let data = generate_chunk_tiles(&block, (0, 0), 99);
let walkable_count = data.tiles.iter().filter(|t| t.walkable).count();
assert!(walkable_count >= 100, "chunk must have ≥100 walkable tiles for NPC routing");
}
#[test]
fn chunk_edges_walkable() {
// Ensure no chunk boundary is fully blocked (prevents dead zones at chunk seams)
let data = generate_chunk_tiles(&block, (0, 0), 99);
// at least 4 walkable tiles on each edge
}
#[test]
fn chunk_deterministic() {
let d1 = generate_chunk_tiles(&block, (0, 0), 42);
let d2 = generate_chunk_tiles(&block, (0, 0), 42);
assert_eq!(d1.tiles, d2.tiles);
}
```
For visual/rendering validation: a Gauntlet room with a generated district. Run `--test-mode SR_LIVE=1` and walk the tiles manually. Per CLAUDE.md, new room added — existing Gauntlet rooms are not modified.
### End-to-end integration
Spawn a player in a generated city. Verify:
1. All chunks within `ChunkLoadRadius` load without panic
2. Player can move (walkability map is non-empty)
3. Second load produces identical tile layout (determinism check)
This is a live server test using existing `--test-mode` infrastructure.
---
## Effort Estimates — Revised
My Round 1 estimate (9 days) was contaminated by heritage grammar work that is now out of scope. Revised for spatial-only:
| Item | Effort | Notes |
|------|--------|-------|
| Fix WorldTier enum + test updates | 0.5d | One enum change, propagate through compilation errors |
| Add missing DistrictSkeleton fields as stubs | 0.5d | Type-only change; no logic |
| Replace String stubs with real types (Stages 1-2 scope) | 1d | `DistrictContext`, `DistrictBoundaries`, etc. — enough to implement Stages 1-2 |
| City-to-district decomposition | 1d | Pure function, well-defined algorithm, unit-testable |
| Server startup: read atlas data → build DistrictMap | 0.5d | Reading systems.db + markers.json; already have DB access patterns |
| Phase 1 generation (Stages 1-2 only) | 1.5d | Classification lookup + seeded block grid |
| `GeneratorChunkData` type upgrade (Vec<bool> → TileEntry grid) | 0.5d | Type change + update `new_walkable()` caller |
| Phase 2 minimal tile generation | 1.5d | Floor/wall placement from density, seeded rectangles |
| Chunk streaming wiring | 0.5d | `DistrictMap` resource + dispatch in `load_chunk()` |
| Integration test (walkable generated city) | 0.5d | Gauntlet room + live test |
**Total: ~8 days** — but only ~6 of those are truly sequential (critical path). The type changes and test writing can overlap with algorithm work.
**Minimum parallelizable scope:** If two developers were available:
- Dev A: Layers 6-7 (city decomp + Phase 1)
- Dev B: Layers 8-9 (Phase 2 + streaming wiring, stubbing Phase 1 output)
Parallelized: ~4 days wall-clock.
### Reconciling with Gestalt's estimate
Gestalt's 5-7 days appears to assume the String stub replacements are free (fold into Phase 1 work) and the startup/DistrictMap wiring is minimal. Both are reasonable simplifications. The key actual divergence: I have explicit line items for the type upgrade (`GeneratorChunkData`) and the server startup path that Gestalt may have assumed as already wired. Neither of us included heritage grammar — that was the bulk of the Round 1 overestimate.
Gestalt's floor estimate (~5 days) represents the fastest possible path with no unexpected compilation surprises from the String-to-real-type migrations. My 8 days is the conservative estimate including that friction. True answer is somewhere between: **~6-7 days** under reasonable conditions.
---
## Open Question Resolutions
### OQ-R1-D (Atlas coordinate translation)
Resolved: there is no pixel-to-sim-tile formula. The atlas coordinates are UI-only. The walkable world uses city-local coordinates starting at (0, 0). See "Core Architecture Insight" section above.
### OQ-R1-C (City marker schema)
Resolved: `primary_function`, `planet_class`, `settlement_pattern` are read from `systems.db` bodies table by cross-reference on `body_id`. `generate_atlas.py` does not need modification.
### OQ-R1-E (Effort estimate)
Resolved: ~6-7 days for minimum spatial vertical slice (spatial only, no heritage grammar, no social sites). Conservative estimate is 8 days including type migration friction.
### OQ-R1-F (#615 cascade position)
Per lead directive: #615 is NPC territory and out of scope for this workshop. No position taken.
---
## What This Proposal Defers
To be explicit about what is NOT in scope for the vertical slice:
- Heritage grammar (Phase 2 tile aesthetics beyond floor/wall)
- Room templates (rectangular buildings replace proper room grammar)
- DistrictSkeleton Stages 3-5 (reservations, social sites, guarantee audit)
- Building interiors with furniture or objects
- Vertical structure / z-levels beyond ground floor
- MobileChunk
- NPC placement
None of these are needed for the question "can the player walk through a generated district?" They can be layered in after the spatial plumbing exists.
@@ -0,0 +1,598 @@
---
title: "Tyre — Round 3: Phase 3 Technical Architecture"
description: "Phase 3 technical layers between atlas output and Phase 5 tile generation — algorithms, tooling vs. runtime split, data formats, LoD architecture, testing, effort, and pipeline question answers"
type: workshop
status: active
workshop: generation-cascade
agent: tyre
round: 3
created: 2026-04-30
---
# Generation Cascade — Round 3: Phase 3 Technical Architecture
**Tyre — Technical Architect**
---
## Scope Declaration
This round corrects the framing from Rounds 1-2. Rounds 1-2 designed the Phase 5 (tile/chunk/district) pipeline. Phase 3 — "Planetary/moon maps and station layouts — cities, rivers, mountains, roads, biomes, rail" — was not designed. This document addresses the missing intermediate layer.
The resolved decisions from Rounds 1-2 (WorldTier enum, SeedChain, city-local coordinates, GeneratorChunkData upgrade, DistrictSkeleton) are treated as given facts about Phase 5. I am not revisiting them. I am designing what Phase 3 must produce *before* Phase 5 can run.
---
## The Architectural Gap — What Phase 3 Is
The atlas pipeline currently ends here:
```
atlas_cities: {city_id, body_id, name, kind, center_row, center_col, population}
atlas_roads: {road_id, body_id, path(64 pixel coords), kind}
```
A city is a dot. A road is a list of pixel coordinates on a 512×256 grid. This is sufficient for Phase 1 (wiki display) but insufficient for:
- **The Atlas of the Reach** (Phase 3 deliverable) — the implant app that shows regional maps, city extents, road connectivity, biome zones
- **Phase 5 generation inputs** — the district skeleton generator needs to know: what biome surrounds this city? How large is it? What is its political character? What roads enter from which directions?
Phase 3 is the tooling work that enriches the atlas data from "dots on a heightmap" to "a browsable regional atlas with classification, footprint, and connectivity."
**Phase 3 lives entirely in Python tooling and systems.db.** It does not add Rust runtime code. The Rust server reads the enriched systems.db at startup; all enrichment computation runs offline in the asset pipeline.
---
## Phase 3 Technical Layers
### Layer A — City Classification and Footprint
**Algorithm:** Derived from existing systems.db data. No new simulation required.
```python
# Inputs (all already in systems.db bodies + atlas_cities tables):
# population, settlement_pattern, economic_role, planet_class, world_tier
#
# Outputs:
# footprint_radius_km — derived from population density
# political_archetype — derived from economic_role, optional authored override
# prosperity_index — derived from WorldTier + DistrictType distribution + economic data
def compute_footprint_radius_km(population: int, settlement_pattern: str) -> float:
# Urban density base: ~8000 people/km² for concentrated, ~500 for dispersed
density = {"urban_concentrated": 8000, "dispersed_rural": 500, "domed": 50000, "cave": 100000}
d = density.get(settlement_pattern, 4000)
return math.sqrt(population / (math.pi * d))
def derive_political_archetype(economic_role: str, city_kind: str) -> str:
# economic_role from bodies table → archetype
# CompanyTown | AdminCapital | FreePort | Contested | OrganicGrowth
mapping = {
"corporate_extraction": "CompanyTown",
"administrative_center": "AdminCapital",
"transit_hub": "FreePort",
"mixed_economy": "OrganicGrowth",
}
return mapping.get(economic_role, "OrganicGrowth")
```
**Tooling vs. Runtime:** Python tooling (`generate_regional.py`). All computation runs offline.
**Data format / storage:** New columns on `atlas_cities` table:
```sql
ALTER TABLE atlas_cities ADD COLUMN footprint_radius_km REAL;
ALTER TABLE atlas_cities ADD COLUMN political_archetype TEXT;
ALTER TABLE atlas_cities ADD COLUMN prosperity_index REAL;
ALTER TABLE atlas_cities ADD COLUMN prosperity_override REAL; -- NULL = always derived
```
**SeedChain:** None. This data is deterministically computed from stored inputs. No randomness.
**Scale:** A city of 500k people at urban density ≈ 7.9km radius ≈ ~0.8 pixels on the 512×256 atlas grid. This is correct — cities are subpixel at atlas scale, which is why they're stored as dots.
---
### Layer B — Regional Biome Grid
**Algorithm:** Downsample the planet_simulation biome grid from 512×256 to a 64×32 regional grid. Each regional cell (8×8 atlas pixels) gets the plurality biome class from the underlying simulation output, plus terrain statistics (mean elevation, mean moisture, terrain roughness).
```python
def generate_regional_biome_grid(terrain: dict) -> list[dict]:
"""Downsample 512×256 biome grid to 64×32 regional cells.
Each cell represents ~8 pixels of planetary surface.
Returns list of {row, col, biome_class, mean_elevation, mean_moisture,
terrain_roughness, is_coastal} dicts.
"""
biome = terrain["biome"] # int8 (256, 512)
elevation = terrain["elevation"] # float32 (256, 512)
moisture = terrain["moisture"] # float32 (256, 512)
surface_water = terrain["surface_water"] # bool (256, 512)
cells = []
for gr in range(32):
for gc in range(64):
# 8×8 pixel window
r0, r1 = gr * 8, (gr + 1) * 8
c0, c1 = gc * 8, (gc + 1) * 8
b_patch = biome[r0:r1, c0:c1].ravel()
e_patch = elevation[r0:r1, c0:c1]
m_patch = moisture[r0:r1, c0:c1]
w_patch = surface_water[r0:r1, c0:c1]
# Plurality biome
unique, counts = np.unique(b_patch, return_counts=True)
dominant_biome = int(unique[counts.argmax()])
# Coast detection: land cells touching water cells
is_coastal = bool(w_patch.any() and (~w_patch).any())
cells.append({
"grid_row": gr, "grid_col": gc,
"biome_class": dominant_biome,
"mean_elevation": float(e_patch.mean()),
"mean_moisture": float(m_patch.mean()),
"terrain_roughness": float(np.gradient(e_patch).std()),
"is_coastal": is_coastal,
"water_fraction": float(w_patch.mean()),
})
return cells
```
**Why 64×32?** Each cell maps to a region small enough to have a single dominant character, large enough to be distinct (8×8 px = planetary feature scale). Gives 2048 cells per body — one SQL row per cell is 2048 rows, trivially queryable. If storage is a concern: pack as a JSON blob on `atlas_body_grids` instead.
**Tooling vs. Runtime:** Python tooling, runs during `generate_atlas.py` pipeline (terrain is already in memory at that point — no second simulate() call needed).
**Data format / storage:** New table:
```sql
CREATE TABLE IF NOT EXISTS atlas_regional_biomes (
body_id TEXT NOT NULL REFERENCES bodies(body_id),
grid_row INTEGER NOT NULL, -- 0-31
grid_col INTEGER NOT NULL, -- 0-63
biome_class INTEGER NOT NULL,
mean_elevation REAL NOT NULL,
mean_moisture REAL NOT NULL,
terrain_roughness REAL NOT NULL,
is_coastal INTEGER NOT NULL DEFAULT 0, -- boolean
water_fraction REAL NOT NULL DEFAULT 0.0,
PRIMARY KEY (body_id, grid_row, grid_col)
);
```
**SeedChain:** None. Deterministically derived from terrain.
**Phase 5 consumption:** When Phase 5 generates districts for a city, it queries `atlas_regional_biomes` at the city's center position (converted from atlas pixel to regional grid cell: `gr = center_row // 8, gc = center_col // 8`) to get surrounding biome context. This tells the district skeleton generator whether wilderness districts around the city are desert, forest, tundra, etc.
---
### Layer C — Road Network Graph
**Algorithm:** Convert existing pixel-path roads/railroads from `atlas_roads` / `atlas_railroads` into a proper graph structure. Node = city center or path junction. Edge = road segment with distance and kind.
```python
def build_road_graph(cities: list[dict], roads: list[dict]) -> tuple[list, list]:
"""Convert pixel-path roads to a navigable graph.
Node: city or junction point at a path endpoint.
Edge: road segment between two nodes, with euclidean distance in atlas pixels
and road kind (commercial, highway, rail).
Returns (nodes, edges).
"""
nodes = []
node_index = {}
# Cities are always nodes
for city in cities:
nid = len(nodes)
node_index[(city["_row"], city["_col"])] = nid
nodes.append({
"node_id": nid,
"kind": "city",
"ref_id": city["id"],
"row": city["_row"],
"col": city["_col"],
})
edges = []
for road in roads:
path = road["path"] # [[row, col], ...]
if len(path) < 2:
continue
start = tuple(path[0])
end = tuple(path[-1])
# Snap endpoints to nearest city node
start_nid = _snap_to_node(start, nodes, threshold=15)
end_nid = _snap_to_node(end, nodes, threshold=15)
if start_nid is None or end_nid is None:
continue
# Distance: sum of euclidean segments along path
dist = sum(
math.sqrt((path[i][0] - path[i-1][0])**2 + (path[i][1] - path[i-1][1])**2)
for i in range(1, len(path))
)
edges.append({
"from_node": start_nid,
"to_node": end_nid,
"kind": road.get("kind", "commercial"),
"distance_px": dist,
})
return nodes, edges
```
**Tooling vs. Runtime:** Python tooling, runs after `generate_atlas.py` has placed cities and roads.
**Data format / storage:** Two new tables:
```sql
CREATE TABLE IF NOT EXISTS atlas_road_nodes (
node_id TEXT PRIMARY KEY, -- "{body_id}/{local_id}"
body_id TEXT NOT NULL,
kind TEXT NOT NULL, -- 'city' | 'junction'
ref_id TEXT, -- city local_id if kind='city'
grid_row INTEGER NOT NULL,
grid_col INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS atlas_road_edges (
edge_id TEXT PRIMARY KEY,
body_id TEXT NOT NULL,
from_node TEXT NOT NULL REFERENCES atlas_road_nodes(node_id),
to_node TEXT NOT NULL REFERENCES atlas_road_nodes(node_id),
kind TEXT NOT NULL, -- 'commercial' | 'highway' | 'rail'
distance_px REAL NOT NULL
);
```
**SeedChain:** None. Derived from stored city positions and road paths.
**Phase 3 Atlas UI use:** The implant atlas app can render the road graph as a connectivity overlay on the planetary map. This is richer than raw pixel paths — the client can query "what roads connect city A to city B" without scanning all pixels.
**Phase 5 use:** District access point placement uses this graph to know which compass directions roads enter the city from. A city at the end of a single road gets access points on that one edge; a transit hub at a road junction gets access points on multiple edges.
---
### Layer D — Station Module Topology
**Algorithm:** Stations have no terrain. Their layout is generated from a schema derived from `economic_role`, `population`, and `settlement_pattern = "orbital_only"`. A station is decomposed into modules with a connection topology.
Module types:
- `HabitatRing` — residential / crew quarters
- `DockBay` — ship berths, cargo handling
- `CommerceHub` — trade, market, services
- `IndustrialSection` — manufacturing, processing
- `AdminCore` — command, administration
- `SecuritySection` — law enforcement, detention
Connection topology: a graph of module adjacency. Each edge has a passage type (`Pressurized | Airlock | EVA | Service`).
```python
def generate_station_topology(station: dict) -> dict:
"""Generate module list and connection graph for an orbital station.
economic_role drives which modules are present and their relative sizes.
population drives total habitable volume (module count).
"""
economic_role = station["economic_role"]
population = station["population"]
# Module count scales with population (log-ish)
module_count = max(3, int(math.log10(max(population, 100)) * 2))
role_templates = {
"transit_hub": ["DockBay", "DockBay", "CommerceHub", "AdminCore", "HabitatRing"],
"corporate_extraction": ["IndustrialSection", "DockBay", "AdminCore", "HabitatRing"],
"administrative_center": ["AdminCore", "AdminCore", "HabitatRing", "SecuritySection"],
"mixed_economy": ["HabitatRing", "CommerceHub", "DockBay", "AdminCore"],
}
template = role_templates.get(economic_role, ["HabitatRing", "DockBay", "AdminCore"])
modules = (template * ((module_count // len(template)) + 1))[:module_count]
# Linear + hub topology: AdminCore in center, others radiate out
edges = []
admin_idx = next((i for i, m in enumerate(modules) if m == "AdminCore"), 0)
for i, module in enumerate(modules):
if i != admin_idx:
edges.append({
"from": admin_idx,
"to": i,
"passage_type": "Pressurized" if module != "DockBay" else "Airlock",
})
return {"modules": modules, "edges": edges}
```
**Tooling vs. Runtime:** Python tooling, runs during `generate_regional.py`.
**Data format / storage:**
```sql
CREATE TABLE IF NOT EXISTS atlas_station_modules (
module_id TEXT PRIMARY KEY, -- "{body_id}/module_{index}"
body_id TEXT NOT NULL,
module_index INTEGER NOT NULL,
module_type TEXT NOT NULL,
PRIMARY KEY (body_id, module_index)
);
CREATE TABLE IF NOT EXISTS atlas_station_connections (
connection_id TEXT PRIMARY KEY,
body_id TEXT NOT NULL,
from_module INTEGER NOT NULL,
to_module INTEGER NOT NULL,
passage_type TEXT NOT NULL -- 'Pressurized' | 'Airlock' | 'EVA' | 'Service'
);
```
**SeedChain:** None. Deterministically derived from economic_role + population.
**Note:** Stations have `settlement_pattern = "orbital_only"` in the bodies table, so `generate_atlas.py` skips city placement for them. `generate_regional.py` handles them as a separate code path.
---
### Layer E — Wilderness and Countryside Annotation
**Algorithm:** For each body, identify the inter-city regions and annotate them with land-use classifications. This is lightweight: sample the regional biome grid (Layer B) at positions between cities and classify based on biome class + proximity to cities.
Land use classes: `Agricultural | Wilderness | Industrial | Wasteland | Ocean | Impassable`
```python
def annotate_wilderness_regions(
cities: list[dict],
regional_biomes: list[dict], # 64×32 grid from Layer B
road_graph: dict, # from Layer C
) -> list[dict]:
"""Annotate non-city regional cells with land use.
Cells within city footprint radius: mark as 'city' (excluded from wilderness).
Cells along roads: mark as 'corridor'.
Remaining cells: classify from biome + proximity to cities.
"""
annotations = []
for cell in regional_biomes:
if cell["water_fraction"] > 0.6:
land_use = "Ocean"
elif cell["biome_class"] in (17, 26): # ice biomes
land_use = "Impassable"
elif _within_city_footprint(cell, cities):
land_use = "Urban"
elif _near_road(cell, road_graph, threshold=2):
land_use = "Corridor"
else:
land_use = _biome_to_land_use(cell["biome_class"], cell["mean_moisture"])
annotations.append({
"grid_row": cell["grid_row"],
"grid_col": cell["grid_col"],
"land_use": land_use,
})
return annotations
```
**Tooling vs. Runtime:** Python tooling, runs during `generate_regional.py`. Reuses Layer B + C output.
**Data format / storage:** Add `land_use` column to `atlas_regional_biomes` rather than a separate table (it's a property of the same cell).
**Phase 5 use:** When generating wilderness districts, Phase 5 queries this annotation to know whether to produce Agricultural, Wilderness, or Industrial district types for chunks outside city bounds.
---
## The Scale Problem — Concrete Answer
**Planet scale:** 512×256 atlas pixels = entire planetary surface. Cities are sub-pixel dots.
**Regional scale (Phase 3):** 64×32 cells = 8px per cell. A city footprint of 7.9km radius (500k population) spans roughly 0.8 atlas pixels = 0.1 regional cells. At regional scale, cities are still point features — the footprint is an attribute, not a spatial extent in the regional grid.
**City scale (Phase 5 input):** N districts, each 512×512 sim tiles. A district spans ~256m. A city of 500k has ~10 districts = a 3-4 district grid.
**The scale gap between regional and city doesn't need a smooth interpolation.** It's a conceptual jump:
- Regional query: "What biome is near this city?"
- City query: "How many districts does this city have, and what are their types?"
These are answered by different data structures. There is no intermediate "regional grid of sim tiles" — that would be Phase 5's job when the player enters the city. The Phase 3 regional data just provides the *context* for Phase 5.
**What Phase 3 passes to Phase 5 per city:**
```rust
pub struct CityGenerationContext {
pub city_id: String,
pub political_archetype: PoliticalArchetype,
pub prosperity_index: f32,
pub surrounding_biome: BiomeClass, // from regional grid at city position
pub road_entry_directions: Vec<CardinalDirection>, // from road graph
pub footprint_radius_km: f32,
}
```
This struct — populated from systems.db at game startup — is the handoff from Phase 3 to Phase 5. Phase 5 uses it as input to district skeleton generation (D-C4, D-C8 from Round 2).
---
## LoD Architecture at Regional Scale
The door-per-edge + descriptor + catalog principle from Phase 5 extends cleanly to all scales:
| Scale | LoD "closed" state | LoD "open" threshold | What opens |
|-------|-------------------|---------------------|------------|
| **Galactic** | Star system = entry in bodies table | Visit system | Planet surfaces visible |
| **Planetary** | Planet = heightmap dot in atlas UI | Player approaches (orbit/landing) | Regional biome overlay activates |
| **Regional** | Region = biome cell on 64×32 grid | Player enters city range | City footprint + district grid activates |
| **City** | City = footprint circle on regional map | Player enters city | Districts rendered as labeled blocks |
| **District** | District = block label on city map | Player enters district | DistrictSkeleton generates on demand |
| **Building** | Building = door-per-edge boundary | Player at door threshold | Descriptor + catalog populates interior |
| **Room** | Room = locked/closed door | Player opens door | Tile grid visible and walkable |
This cascade is architecturally consistent. Every layer closes to the minimum representation possible for entities outside player range. The Phase 3 deliverable (Atlas of the Reach implant app) is the interface for the Planetary and Regional levels.
**Concrete rule for Phase 3 → Phase 5 transition:** When the player's walkable position approaches within `city.footprint_radius_km` of a city center (converted via the appropriate scale factor), Phase 5 generates the city's district grid. Before that threshold, the city exists only as atlas data. The threshold can be implemented as a simple distance check against the city's metadata in systems.db.
---
## Testing Strategy Per Layer
**Layer A (City Classification):**
- Unit test: given known `{population, settlement_pattern, economic_role}`, assert correct `footprint_radius_km`, `political_archetype`, `prosperity_index`
- Fixture: Van Maanen's Star capital — confirm its values match lore expectations
- Regression: run on all inhabited bodies, assert no NULL values and prosperity_index ∈ [0.0, 1.0]
**Layer B (Regional Biome Grid):**
- Unit test: given a synthetic 512×256 biome array, assert downsampled 64×32 output has correct plurality logic
- Regression: run `generate_atlas.py --body GJ380c --dry-run` with the new regional sampling — compare output against a committed fixture (similar to how `check-systems-db-stamp` works). The planet_simulation output is deterministic from seed, so this fixture is stable.
- Validation: assert coastal cells correctly identify ocean adjacency
**Layer C (Road Graph):**
- Unit test: given 2 cities and a straight-line road between them, assert graph has 2 nodes + 1 edge + correct distance
- Validation: assert graph is connected (all cities reachable from capital) for bodies with ≥2 cities
- Integration: run on full body, assert `road_entry_directions` is non-empty for any city with ≥1 connected road
**Layer D (Station Topology):**
- Unit test: given `economic_role = "transit_hub"`, assert topology contains ≥1 DockBay + 1 AdminCore + valid edge list
- Validation: assert all edges reference valid module indices; assert no disconnected modules (every module reachable from AdminCore)
- Regression: run on all orbital-only bodies
**Layer E (Wilderness Annotation):**
- Unit test: given a 3×3 regional grid with one water cell, assert it gets `"Ocean"` annotation
- Regression: run on full pipeline, assert annotation column is non-NULL for all cells
**Integration — the full pipeline:**
```bash
make regen-db # now includes generate_regional.py
tooling/db/sqlite-query "SELECT COUNT(*) FROM atlas_city_profiles WHERE prosperity_index IS NULL"
# → must return 0
tooling/db/sqlite-query "SELECT COUNT(*) FROM atlas_regional_biomes"
# → must return (number of inhabited bodies) × 2048
```
**What makes Phase 3 layers independently testable:** Each layer's inputs are either (a) already in systems.db from prior layers or (b) the terrain dict which is deterministically produced by planet_simulation.py. No layer depends on runtime game state. All layers can be exercised by running the Python tooling pipeline.
---
## Effort Estimates
| Layer | Work | Effort |
|-------|------|--------|
| A — City classification | Add columns to atlas_cities, implement derive functions, meta stamp update | 1.5d |
| B — Regional biome grid | New table, sampling code integrated into generate_atlas.py, regression fixture | 1.5d |
| C — Road graph | New tables, graph-building code, connectivity validation | 1d |
| D — Station topology | New tables, template-based generator, separate code path in generate_regional.py | 1.5d |
| E — Wilderness annotation | Column addition to atlas_regional_biomes, annotation logic | 0.5d |
| Plumbing | New generate_regional.py script structure, meta stamp, Makefile integration, pre-push hook update | 0.5d |
| Rust read path | CityGenerationContext struct + systems.db query functions | 0.5d |
| **Total** | | **~7 developer days** |
**Important:** The 7-day estimate is independent of the Phase 5 work (~6 days from Round 2). Phase 3 and Phase 5 are parallel workstreams. Phase 3 does not block Phase 5 during development — Phase 5 can be developed against hardcoded stub context values and then wired to systems.db when Phase 3 is complete.
---
## Data Pipeline Decision — systems.db vs. Seed-Derived vs. Middle Ground
**All Phase 3 data belongs in systems.db (committed asset store).** Rationale:
1. **Authoring coupling:** city footprint, political archetype, prosperity_index all derive from wiki-authored bodies data. They belong with that data in the asset store, not in a per-play runtime derivation.
2. **UI dependency:** the Atlas of the Reach implant app reads this data to display the regional map. It must be available before any player has ever started a game session. Seed-derived data requires a game session to generate.
3. **Determinism is not the issue:** Phase 3 data is deterministic (same sources → same output) but determinism alone doesn't justify runtime generation. The atlas city placements are also deterministic, yet we commit them to systems.db via the generator pipeline. Same principle applies here.
4. **Generation cost:** planet_simulation.py takes real compute time. Caching its output at the regional resolution (Layer B) is the right call. Re-running it at game start for every body would be prohibitive.
5. **The "middle ground" case:** there is a legitimate middle ground for *wilderness interior variation* — the exact shape of forests and river tributaries below regional resolution. That detail is NOT Phase 3. It's Phase 5 wilderness district generation, and it correctly belongs in the seed-derived tier.
**Summary of the boundary:**
```
systems.db (committed by generate_regional.py):
atlas_city_profiles — footprint, archetype, prosperity
atlas_regional_biomes — 64×32 biome grid per body + land use
atlas_road_nodes/edges — road graph
atlas_station_modules — station topology
Seed-derived (Phase 5, never stored):
DistrictSkeleton — district layout and blocks
ChunkData — 64×64 tile grids
Wilderness interior — exact forest/river shapes within wilderness districts
```
---
## Open Questions from Round 2 — Answered
**OQ-R2-1: prosperity_index and perimeter_treatment — include in minimum Phase 5 slice?**
YES. Paula's argument stands up technically. `prosperity_index` is a single f32 derived in Stage 1 from inputs already available at classification time (`WorldTier + DistrictType + city.economic_role`). Adding it is 10 lines of Stage 1 code. Not adding it means Phase 2 tile generation produces identical spatial character regardless of district wealth — then retrofitting it is a Phase 2 rewrite.
`perimeter_treatment` is equally cheap: derived from `(DistrictType, WorldTier, prosperity_index)` as a lookup table. Its impact is a single set of boundary tiles at `access_points`. Cost to include now: trivial. Cost to retrofit: every Phase 2 tile generation function gets a parameter it previously ignored.
Both fields belong on DistrictSkeleton before Phase 2 is implemented. Include them.
**OQ-R2-2: political_archetype — formal field or implicit in decomposition formula?**
Formal field, but derived by default. Add `political_archetype TEXT` to `atlas_cities` in Phase 3 (Layer A above). For unnamed cities: derived from `economic_role` by the Python tooling. For named cities: wiki authors can supply an override column value (`political_archetype_override`). The decomposition formula in Phase 5 reads this field; it doesn't derive it inline.
This is cleaner than burying the derivation inside the district generator — the generator should receive a value, not compute lore classifications.
**OQ-R2-3: Phase 2 tile algorithm — door-per-block-edge vs. density-driven rectangle placement?**
Both. They target different levels:
- **door-per-block-edge** defines the block's connectivity skeleton: where are the block boundaries, where do streets cross them, where do buildings face the street
- **density-driven rectangle placement** fills in building footprints *within* the block skeleton
A block first gets its street edges (door-per-block-edge), then buildings are placed as density-appropriate rectangles inside the remaining space. This is how urban blocks actually work. Implement them in that order: skeleton first, fill second.
**OQ-R2-4: Step 2b (full N-district city layout) — minimum slice or follow-on?**
Follow-on. The minimum slice needs 1 district to demonstrate walkability. But the CityGenerationContext struct and the district grid data model must support N districts from day 1 — just don't generate the other districts yet. A `district_grid_width: u8` and `district_count: u8` on CityGenerationContext cost nothing now and prevent a data model migration later.
**OQ-R2-5: Naming register lookup table — before Phase 2 or post-walkable?**
Post-walkable. The naming register is a text overlay — it affects what street signs say, not whether tiles are walkable. It's a `Vec<(DistrictType, WorldTier, NamingRegister)>` lookup that the generator uses when filling the `name` field on blocks and streets. Add it as a follow-on ticket after Phase 2 produces a walkable district.
**OQ-R2-6: prosperity_index for named cities — authored or derived?**
Derived by default, authored override allowed. Add `prosperity_override REAL` column to `atlas_cities` (NULL = derived). The wiki author of Van Maanen's Star can supply an explicit value if the lore demands it. The Phase 3 generator checks: `COALESCE(prosperity_override, <derived_formula>)`. Zero special-casing required.
**OQ-R2-7: Naming register table ownership?**
Paula proposes the register taxonomy. Tyre reviews against spatial constraints (does the register system produce contradictions for unusual WorldTier + DistrictType combos?). Mellanie populates name pools within each register. Lead locks the lookup table as a D-record. The implementation is a static TOML or RON file consumed by the Phase 5 district generator.
---
## Generator Pipeline Integration
Phase 3 introduces a new generator: `generate_regional.py`. The `make regen-db` pipeline order becomes:
```
1. import_economics.py (existing — economic data, brands)
2. generate_atlas.py (existing — city placement, heightmaps, road pixel-paths)
3. generate_regional.py (NEW — city classification, regional biomes, road graph, station topology)
```
`generate_regional.py` follows the same generator convention as `generate_atlas.py`:
- Writes a meta stamp row to the `meta` table
- Includes itself in the `GENERATOR_SOURCES` dict in `tooling/check-systems-db-stamp`
- The pre-push hook validates its stamp alongside the existing generators
The `/pr-push` skill's source-file watch list must also be updated to include `generate_regional.py`.
**Incremental mode:** `generate_regional.py --body GJ380c` processes only one body (same as generate_atlas.py). `--force` overwrites existing regional data. `--dry-run` prints what would be written without touching the DB.
---
## Summary
Phase 3 is a tooling and data enrichment phase. It produces:
- 5 new or modified systems.db tables
- 1 new Python generator (`generate_regional.py`)
- `CityGenerationContext` Rust struct (read path only)
It bridges atlas output (city dots) to Phase 5 input (district generation context). It does not generate walkable tiles — that is Phase 5's job. What it does is give Phase 5 enough structured context to generate districts that are spatially appropriate to their location, biome, and political character.
Estimated effort: **~7 developer days**, independent of and parallel to Phase 5 work (~6 days).
The Phase 3 deliverable — the Atlas of the Reach implant app — consumes this enriched systems.db data to display regional maps, city footprints, road connectivity, and biome zones. The atlas UI implementation is client-side work (stig's territory) that reads the tables Phase 3 produces.
@@ -0,0 +1,442 @@
---
title: "Workshop Outcomes — Generation Cascade Workshop"
description: "Authoritative record of the generation-cascade workshop: original intent, scope corrections, round-by-round journey, all resolved decisions, what was locked and deferred, and how the workshop output became a new workshop brief"
type: workshop
status: complete
workshop: generation-cascade
agent: qatux
created: 2026-04-30
---
# Generation Cascade Workshop — Outcomes
**Compiled by:** Qatux
**Dates:** 2026-04-30 (all four rounds)
**Status:** Complete. Output delivered. Successor workshop brief produced.
---
## What This Document Is
This is the authoritative record of the generation-cascade workshop. It documents the full journey — what the workshop set out to do, what changed and why across each round, what was resolved, what was deferred, and how the output became input to the next workshop.
The technical decisions are captured in detail in the per-round notes files. This document captures the narrative: why this workshop ran, what shape it took, and what came out of it.
---
## Original Intent
The generation-cascade workshop was initiated to answer a specific question: **can we implement the generation pipeline now, and if so, what's blocking us?**
The context: character creation work (#694) and apartment generation (#681) were queued. Before committing implementation capacity to these systems, the team needed to know whether the world generation pipeline that would feed them was solid.
**The brief framing:** A 3-round implementation audit using a city-outward approach — start from the walkable city environment that the player would inhabit, trace every generation layer the city required, and determine which layers were implemented, which were stubs, and which were missing entirely.
**The blocking intent:** Several tickets (#681, #694, character apartment work) would not be formally unblocked until the generation pipeline was either proven complete or proven to have a clear implementation path with scoped tickets.
**Initial participants:** Gestalt (systems design), Tyre (technical architecture), Miri (worldbuilding and content).
---
## Round 1: The Inventory
**Question:** What does the pipeline look like? What works, what's stubbed, what's missing?
### What the round found
All three participants independently converged on the same finding: the pipeline has a hard operational cliff at Layer 5 (atlas markers). Everything above the cliff — galactic definitions, planetary heightmaps, city placement, economics layer — is working production code. Everything below the cliff is either missing or compiled stubs with no generation logic.
The two critical missing layers:
- **Layer 6: City → District decomposition** — no design, no code, no ticket. The missing link between atlas output and walkable world.
- **Layer 9: Phase 2 ChunkData tile generation** — entirely absent. `GeneratorChunkData = Vec<bool>`. No tile vocabulary. No fill logic.
Additional findings:
- `WorldTier` enum in `generator.rs` uses wrong values (`Peripheral | Connected | Core`) instead of the workshop-canonical values (`Epicenter | Regional | Backwater | Passage | Waypoint`). Hard blocker for all downstream work.
- `DistrictSkeleton` fields compile but are `pub type Foo = String` — zero generation logic.
- The triangle system (`triangle.rs`) is substantially more complete than the brief indicated — it was incorrectly labeled as stubs. Wiring gap, not code gap.
- `chunk_streaming.rs` is architecturally sound and tested; `load_chunk()` just creates blank placeholders instead of calling a generator.
### Content gaps (Miri)
Miri's audit identified content gaps orthogonal to the code gaps:
- Heritage root blend specification missing — culture RON files don't include heritage blend weights
- `HeritageGrammarOverlay` data files don't exist for any of the 10 heritage roots
- Zone identity definitions missing for 7 of 9 DistrictType values
- City marker schema insufficient for Phase 1 inputs (`primary_function`, `planet_class`, `settlement_pattern` not in markers.json)
**Round 1 open questions:** 8 items (OQ-R1-A through OQ-R1-H). Critical blockers: heritage root count disagreement (7 vs. 10), `SocietyProfileRef` type design, atlas coordinate translation question.
---
## Scope Correction: Between Round 1 and Round 2
**The lead intervened between rounds to restructure scope.**
Before Round 2 launched, the lead issued a directive removing the following from workshop scope:
- Heritage grammar overlay (#615, all associated heritage content)
- NPC population systems
- Social sites generation
- #615 (Tycoon starting state) — treated as out of scope for this workshop entirely
**Why this mattered:** Tyre's Round 1 estimate (~9 dev-days for the minimum vertical slice) was contaminated by heritage grammar scope. Once removed, estimates would converge. More importantly, the city-outward audit had been dragging attention to Phase 6 content (cultural room grammar, NPC bundles, heritage overlay) when Phase 5 walkable world generation hadn't been designed yet.
**What got dropped from the participants list:** Miri did not have a role in Round 2 or later. Her content audit (Round 1) was valid, but the workshop pivoted to spatial pipeline work.
**Round 2 added:** Paula (narrative + political depth), who had spatial analysis relevant to the pipeline that neither Gestalt nor Tyre were covering.
---
## Round 2: The Spatial Pipeline
**Question:** Given spatial-only scope, what does the implementation order look like? What are the key architecture decisions?
**New participant:** Paula (narrative/political depth, spatial signals in generation).
### Technical resolutions
Two Round 1 open questions resolved by independent convergence:
**OQ-R1-C (City marker schema):** Do not extend markers.json. `primary_function`, `planet_class`, `settlement_pattern` are read from systems.db bodies table by cross-reference on `body_id`. markers.json stays as a pure spatial + naming store.
**OQ-R1-D (Atlas coordinate translation):** No formula needed. Atlas coordinates are UI-only (display coordinate system for the atlas UI). The walkable world uses city-local sim-tile coordinates from (0,0). Districts in a city are placed at `(col × 512, row × 512)` sim tiles in city-local space.
**Stored-vs-derived boundary locked:** Atlas markers are the last stored layer. Everything at Layer 6 and below is seed-derived at runtime. Save files store: `world_seed` + markers snapshot hash + `ChunkMutations` only.
**SeedChain algorithm confirmed:** FNV-1a `child_seed(parent, discriminant)` for all randomness below the atlas layer. Any district or chunk can be generated in isolation without generating neighbors first.
**Effort converged:** ~6 dev-days (spatial scope only, Stages 1-2 minimum). Round 1's 9d estimate was heritage-contaminated.
**Implementation order locked** (5-step sequential chain):
1. Fix WorldTier enum (0.5d)
2. Single-district decomposition stub (0.5d)
3. Phase 1 Stages 1-2 generation (1.5-2d)
4. Phase 2 minimal tile generator (1.5d)
5. Chunk streaming wiring (0.5-1d) → **Minimum Viable Walkable World**
### Paula's additions
Paula introduced four spatial concepts not addressed by Gestalt or Tyre. These required Round 3 disposition:
- `prosperity_index: f32` on DistrictSkeleton — scalar driving tile decay, road width, building density, green space
- `perimeter_treatment: PerimeterTreatment` on DistrictSkeleton — boundary marker enum (Open/Fenced/Walled/Gated/Checkpoint)
- `political_archetype` per city — formal derived field (CompanyTown/AdminCapital/FreePort/Contested/OrganicGrowth)
- Naming registers — `(DistrictType, WorldTier) → register` lookup table; name pool population deferred as cultural content
Paula's argument for including `prosperity_index` and `perimeter_treatment` before Phase 2: if they're not on DistrictSkeleton before Phase 2 is implemented, tile generation produces spatially indistinguishable spaces regardless of district type — and retrofitting is a Phase 2 rewrite.
### D-record candidates from Round 2 (D-C1 through D-C8)
| ID | Decision |
|----|---------|
| D-C1 | Generation pipeline layer definitions (canonical layer sequence with storage policies) |
| D-C2 | Stored-vs-derived transition — atlas markers are the last stored layer |
| D-C3 | Phase 1 minimum spatial scope — Stages 1-2 mandatory, Stages 3-5 deferred |
| D-C4 | City → district decomposition formula (population/50,000 + WorldTier ceilings) |
| D-C5 | City-local coordinate system for districts (atlas is UI-only; no pixel-to-tile formula) |
| D-C6 | SeedChain algorithm — FNV-1a `child_seed(parent, discriminant)` |
| D-C7 | `GeneratorChunkData` type upgrade — `Vec<bool>``Vec<TileEntry>` with `tile_id` + `walkable` |
| D-C8 | Spatial class legibility requirements — `prosperity_index`, `perimeter_treatment`, `layout_mode` on DistrictSkeleton (Paula; not yet confirmed by Gestalt/Tyre) |
**Round 2 open questions:** 7 items (OQ-R2-1 through OQ-R2-7). Most critical: prosperity_index and perimeter_treatment inclusion (OQ-R2-1), political_archetype formality (OQ-R2-2), Phase 2 tile algorithm choice (OQ-R2-3).
---
## Round 3: The Phase 3 Reframe
**Question:** What does Phase 3 (the map/atlas layer, before Phase 5 runtime generation) actually need to produce?
**New participant:** Burnelli-Sheldon (economics + simulation modeling), added to provide quantitative economic grounding for the district generation bridge.
### The corrective
Early in Round 3, Tyre identified a structural problem with the workshop framing:
> "Rounds 1 and 2 were auditing and designing Phase 5 (runtime tile generation). Phase 3 — which produces the regional maps that Phase 5 consumes — had not been designed at all."
This was correct. The city-outward audit had jumped from atlas markers (Phase 3 output) directly to district generation (Phase 5), skipping the intermediate layer that was supposed to enrich the atlas with regional data: road networks, biome grids, station topology, city classification data.
**The reframe:** Phase 3 requires a new Python generator (`generate_regional.py`) running third in the `make regen-db` pipeline. It enriches systems.db with city classification, regional biome data, road graphs, station topology, and (debated) sub-settlement hierarchy. All Phase 3 computation is offline. Phase 5 reads its output at startup via a Rust struct (`CityGenerationContext`). Both phases are fully parallelizable during development.
### Key consensus
**All four participants independently arrived at the same Phase 3/Phase 5 boundary:**
- Phase 3 = Python tooling, offline, committed to systems.db
- Phase 5 = Rust runtime, seed-derived, never stored
- The two phases do not block each other during development
**All four participants agreed on Phase 1 scope (OQ-R2-1 resolved):** `prosperity_index` and `perimeter_treatment` must be on DistrictSkeleton before Phase 2. Retrofit cost exceeds add-now cost.
**All seven Round 2 open questions resolved** by Round 3.
### Paula's Phase 3 additions
Paula introduced six new spatial/political fields all derivable from existing systems.db data:
- `TerritorialStatus` — six-value enum for regional political classification (CoreTerritory/FrontierTerritory/ExtractiveZone/ContestZone/WildernessBuffer/AbandonedZone)
- `FoundingOrientation` — five-value enum for city founding direction (PortFacing/RailHeadFacing/ResourceFacing/DefenseFacing/AdminFacing)
- `MaintenanceAuthority` on road segments — five-value enum (Administrative/Corporate/Communal/Trade/Abandoned)
- `PoliticalTether` per sub-city settlement — relationship classification to nearest tether city
- `DualNaming`/`RegionalFeatureName` — ContestZone naming conditions
- Settlement hierarchy (town/outpost/waypoint/rural cluster/ruin tiers)
### Burnelli-Sheldon's economics bridge
Burnelli-Sheldon provided:
- 6-field minimum read set — all fields queryable from systems.db per city; sufficient to produce a spatially differentiated Phase 1 skeleton without additional authoring
- 10×9 economic_role → DistrictType weight table — canonical probability weights mapping all 10 `economic_role` values to all 9 DistrictType values
- Three concrete differentiation cases demonstrating how the same DistrictType produces different spatial character from economic data alone (Tributarium/Bluebank/Strata)
- `prosperity_index` derivation formula with `distribution_index = stratified` vs. `moderate` paths
- Three narrow fields requiring post-generation reporting back to the economics simulation (effective port tile count, corp district allocation, shadow economy footprint)
### Disagreements remaining after Round 3
| Gap | Gestalt/Paula position | Tyre's implicit position |
|-----|----------------------|------------------------|
| TerritorialStatus | Dedicated Phase 3 Layer D, in CityGenerationContext | Not addressed; absent from layer design |
| FoundingOrientation | Layer A output, in CityGenerationContext | Absent from struct |
| Sub-settlement hierarchy | `atlas_sub_settlements` table, PoliticalTether derivation | Land-use annotation only |
| MaintenanceAuthority on roads | `atlas_road_edges` schema column | Absent from schema |
### D-record candidates from Round 3 (D-C9 through D-C17)
| ID | Decision |
|----|---------|
| D-C9 | Economic fields → generator parameter mapping (6-field minimum read set, 3-query pattern) |
| D-C10 | `economic_role` → DistrictType weight table (10×9 canonical default) |
| D-C11 | Phase 3 layer sequence and tooling architecture (`generate_regional.py` as third generator) |
| D-C12 | `CityGenerationContext` as Phase 3 → Phase 5 handoff struct (minimum agreed fields) |
| D-C13 | Phase 3 LoD cascade — stored boundary extended to include Phase 3 outputs |
| D-C14 | `generate_regional.py` pipeline integration rules (meta stamp, GENERATOR_SOURCES, flags, order) |
| D-C15 | `TerritorialStatus` enum definition (6 values, derivation algorithm) — Paula/Gestalt; Tyre not yet reviewed |
| D-C16 | `FoundingOrientation` enum (5 values, derivation from settlement_pattern + economic_role) — Paula/Gestalt; Tyre not addressed |
| D-C17 | `MaintenanceAuthority` on road segments (5 values, derivation from endpoint relationships) — Paula/Gestalt; absent from Tyre schema |
**Four lead decisions identified** before Phase 3 schemas could be finalized (OQ-R3-1 through OQ-R3-4):
1. Is `TerritorialStatus` in Phase 3 scope?
2. Is `FoundingOrientation` in Phase 3 Layer A?
3. Does Phase 3 include sub-settlement hierarchy?
4. Does `atlas_road_edges` include `maintenance_authority`?
---
## Round 4: The Planet-Down Reframe
**Question:** Requirements gathering for the next workshop brief. What does each cascade layer need to produce, and for what purpose?
**The lead's reframe:** The lead rejected the city-outward framing at the start of Round 4 and restated the correct causal direction:
> "Planet-down, not city-outward. The world exists before civilization. Civilization is layered onto it. The player enters at the bottom of that stack and reads upward."
This reframe replaced the Phase 3 layer map (which had been designed city-outward as "what does the city need") with a four-layer cascade (which starts from geology and civilizes down to street tiles):
| Layer | What it is |
|-------|-----------|
| **1. Empty World** | Heightmap → water simulation → sub-biome refinement. No civilization. |
| **2. Population Overlay** | Economics onto geography. Settlements anchor at geographic nexus points. |
| **3. City-Level Planning** | Districts, suburbs, industrial zones, ports. The Cities Skylines layer. |
| **4. Street-Level Rendering** | Streets, buildings, walkable tiles. On demand as the player moves. |
### The two governing rules
Gestalt identified two rules that became the architectural spine:
**DETERMINISM RULE:** Economic simulation's rolling state affects RENDERING (prosperity, repair state) but NOT LAYOUT (streets and buildings locked by seed).
**LOD/ZOOM RULE:** When a player spawns, the code "zooms in" on the planet — tracing all cascade steps to where the player sits, filling in street-level detail where the player is. The cascade is lazy; only what the player needs is computed.
### How Round 3's disagreements resolved
The planet-down reframe resolved all four Round 3 lead decisions without requiring a separate decision session:
**OQ-R3-1 (TerritorialStatus scope):** Resolved by Paula. TerritorialStatus is a Layer 2 **post-processing output** — one classification pass after settlement placement and road generation complete. Not a dedicated layer. Costs one pass, not a separate architecture layer. The Gestalt/Tyre debate about Layer D is closed.
**OQ-R3-2 (FoundingOrientation):** Resolved by Paula. In the planet-down cascade, FoundingOrientation is **directly readable from the geographic trigger** that caused the settlement to exist. No authoring pass required. Included in CityGenerationContext.
**OQ-R3-3 (Sub-settlement hierarchy):** Resolved by the lead's cascade description. Sub-settlements are explicitly stated as included in Layer 2 ("Sub-settlements INCLUDED"). Confirmed in Tyre's brief as a Given Fact.
**OQ-R3-4 (MaintenanceAuthority on roads):** Resolved by Paula. MaintenanceAuthority falls directly from road endpoint relationships at road generation time. Derivable at generation — no separate schema decision beyond adding the column.
### Paula's "Geography Is Political History" framing
Paula provided the narrative rationale for the planet-down ordering and connected all accepted proposals to their correct cascade layer positions. Key contributions:
- Layer 1 geographic feature tags (RiverConfluence/CoastalHarbor/MountainPass/ArablePlain/ResourceConcentration/Defensible/NaturalBarrier) as mandatory named settlement attractors
- FoundingOrientation is readable from geographic trigger — no authoring
- TerritorialStatus derivation confirmed as Layer 2 post-processing
- `prosperity_delta` architecture: seed-locked `prosperity_baseline` (what the district was planned as) + sim-driven `prosperity_delta` (what current state says) = ghost city effect narrative
- Ruins as structural permanence: settlement physical structure persists even when economic basis is gone
### Burnelli-Sheldon's latent settlement principle
Under the Determinism Rule, settlement positions must be seed-locked at generation time. But economic conditions change. The reconciliation:
> The generator places ALL economically plausible settlement positions at Layer 2. Whether each settlement is ACTIVE is determined by the economic simulation's rolling state. An inactive settlement exists spatially as ruins or empty structures.
This means ghost towns are generated, not scripted. The road to the closed mine still exists. The buildings of the abandoned outpost are still there.
### Tyre's brief assembly
Tyre assembled all Round 4 material into `docs/workshops/planet-down-cascade/BRIEF.md`. This is the primary deliverable of the generation-cascade workshop.
The brief separates:
- **Given Facts** — all decisions locked by Rounds 1-4, not to be revisited
- **Workshop Sections** — the open design questions the next workshop must resolve
**Notable decisions encoded in Given Facts:**
- CityGenerationContext struct with `prosperity_baseline` field name (resolves the Round 4 naming question: `prosperity_baseline` = seed-locked Layer 3 output; `prosperity_current` = runtime simulation state — these must never be conflated)
- Latent settlement principle
- Determinism boundary table per layer
- LoD zoom-in cascade diagram
- All D-C1 through D-C12 candidates treated as locked
---
## Full D-Record Candidate Register
All D-record candidates produced by this workshop, with current status:
| ID | Decision | Status in planet-down brief |
|----|---------|---------------------------|
| D-C1 | Generation pipeline layer definitions | Incorporated as Given Facts (pipeline architecture section) |
| D-C2 | Stored-vs-derived transition — atlas markers are last stored layer | Given Facts: "Atlas markers are the last stored layer" |
| D-C3 | Phase 1 minimum scope — Stages 1-2 mandatory, Stages 3-5 deferred | Given Facts: "District Skeleton (Phase 1 Minimum Scope)" |
| D-C4 | City → district decomposition formula | Given Facts: "City Decomposition (D-C4)" |
| D-C5 | City-local coordinate system (atlas = UI-only) | Given Facts: "Atlas coordinates are UI-only" |
| D-C6 | SeedChain algorithm (FNV-1a `child_seed`) | Given Facts: "Seed Chain" section with full code |
| D-C7 | `GeneratorChunkData` upgrade to `Vec<TileEntry>` | Given Facts: "GeneratorChunkData Upgrade" |
| D-C8 | Spatial legibility requirements (`prosperity_baseline`, `perimeter_treatment`) | Given Facts: "District Skeleton (Phase 1 Minimum Scope)" — field names updated to `prosperity_baseline` |
| D-C9 | 6-field minimum economics read set | Given Facts: "Economics: 6-Field Minimum Read Set" |
| D-C10 | 10×9 `economic_role` → DistrictType weight table | Given Facts: full table reproduced |
| D-C11 | Phase 3 layer sequence and tooling architecture | Given Facts: "Pipeline Architecture" table |
| D-C12 | `CityGenerationContext` as Phase 3 → Phase 5 handoff struct | Given Facts: full struct definition with `prosperity_baseline` |
| D-C13 | Stored boundary extended to Phase 3 outputs | Given Facts: "Pipeline Architecture" |
| D-C14 | `generate_regional.py` pipeline integration rules | Given Facts: "generate_regional.py is the third pipeline generator" |
| D-C15 | `TerritorialStatus` enum (6 values) | Resolved: TerritorialStatus is Layer 2 post-processing output (Paula Round 4). Enum confirmed. Layer D debate closed. |
| D-C16 | `FoundingOrientation` enum (5 values) | Resolved: directly readable from geographic trigger. In CityGenerationContext (Given Facts struct). |
| D-C17 | `MaintenanceAuthority` on road segments (5 values) | Resolved: derivable at road generation time. Included in Layer 2 Required Outputs. |
**Note on formal D-NNN assignment:** These D-C candidates were assigned permanent IDs (D-194 through D-219) during the planet-down-cascade workshop. See `docs/workshops/planet-down-cascade/workshop-outcomes.md` for the full placeholder-to-real-ID mapping table.
---
## What Was Locked
The following architectural decisions are locked. The planet-down-cascade workshop brief treats them as Given Facts and they must not be revisited:
1. **Phase 3/Phase 5 split:** Python tooling + systems.db / Rust runtime + seed-derived
2. **Phase parallelizability:** Phase 5 can stub `CityGenerationContext` and wire to systems.db when Phase 3 is done
3. **`generate_regional.py` as the third pipeline generator** with meta stamp and pre-push hook integration
4. **Atlas markers as the last stored layer** — everything from Layer 3 down is seed-derived
5. **No markers.json extension** — city attributes via systems.db cross-reference on `body_id`
6. **Atlas coordinates are UI-only** — no pixel-to-sim-tile formula
7. **FNV-1a SeedChain**`child_seed(parent, discriminant)` for all randomness below atlas layer
8. **`CityGenerationContext` struct** with `prosperity_baseline: f32` (seed-locked) — distinct from `prosperity_current: f32` (runtime sim state)
9. **`prosperity_baseline` naming** — the field previously called `prosperity_index` is now `prosperity_baseline` to prevent conflation with runtime state
10. **City decomposition formula**`max(1, floor(population / 50_000))` with WorldTier ceilings
11. **Phase 1 Stages 1-2 minimum** — classification + 4×4 block grid; Stages 3-5 deferred
12. **WorldTier enum**`Epicenter | Regional | Backwater | Passage | Waypoint` (replaces the wrong `Peripheral | Connected | Core`)
13. **`GeneratorChunkData` upgrade** — `Vec<TileEntry>` with `tile_id: TileId` + `walkable: bool`
14. **Minimum tile vocabulary**`floor_street | floor_interior | wall`
15. **6-field minimum read set** — the economics query pattern for Phase 3/Phase 5
16. **10×9 DistrictType weight table** — locked canonical default, tunable but not relitigated per sprint
17. **Sub-settlement hierarchy** — six tiers included as lead confirmation; placement triggers confirmed
18. **Latent settlement principle** — all economically plausible positions placed at Layer 2; active/ghost driven by sim
19. **Determinism boundary per layer** — explicit table of what is seed-locked vs. economics-variable
20. **LoD zoom-in cascade** — lazy cascade from planet level to player tile; `DistrictMap` only exists for cities the player is near
21. **TerritorialStatus as Layer 2 post-processing** — six values, derived after settlement placement + road generation complete
22. **FoundingOrientation from geographic trigger** — directly readable; no authoring required
23. **MaintenanceAuthority on road segments** — derivable from endpoint relationships at generation time
24. **Ghost city effect architecture**`prosperity_baseline` (seed-locked) + `prosperity_delta` (sim-driven) = effective prosperity; the gap is the narrative
---
## What Was Deferred
The following items were proposed, discussed, and explicitly deferred:
**From Phase 1 (DistrictSkeleton):**
- Stage 3: Reservations (MultiBlockReservation, terminal sites)
- Stage 4: Social sites + NPC population
- Stage 5: Guarantee audit
- `FoundingOrientation`-modified district placement grid (gradient direction only in minimum slice)
- Explicit arterial road layout within city (district boundaries = implicit streets in minimum slice)
- Topographic constraints per district (all cities treated as flat in minimum slice)
**From Phase 2 (tile generation):**
- Tile condition overlays (economics-variable rendering) — implement after walkable world exists
- Building interior tile generation beyond door-per-edge boundary
- Prop/decal scatter and entity spawn points
- Heritage grammar overlay (removed from scope entirely — Phase 6 territory)
**From Phase 3 (`generate_regional.py`):**
- Agricultural dispersed node exact placement (land_use tag sufficient for minimum)
- Shadow node placement (needs `collection_efficiency` data quality improvement first)
- DualNaming/common_name generator vs. authored-only
**From the economics layer:**
- `behavioral_archetype` backfill pass in `import_economics.py` (needed before Layer 3 but not blocking Layer 2)
- `scope` field normalization on corporations table
**Naming registers:**
- Lookup table `(DistrictType, WorldTier) → register` deferred to post-walkable world
- Name pool population deferred entirely (cultural/heritage content, not spatial pipeline work)
---
## Open Questions Carried into the Next Workshop
These are the questions the planet-down-cascade workshop must answer before Layers 1-4 can be implemented:
**Must be resolved before Layer 1:**
- **L1-Q1:** Authored rivers vs. seeded drainage network — what is the relationship? Do authored markers.json river polylines get replaced by generated drainage, or do they anchor the generated network?
- **L1-Q2:** River resolution — regional-only (64×32) or intermediate resolution? Determines whether river data belongs in systems.db or is seed-derived at Phase 5.
- **L1-Q3:** Stored vs. derived for river data — confluence points must be queryable before Layer 2 runs.
**Must be resolved before Layer 2:**
- **L2-Q1:** City positions — anchored with geographic explanation attached (generate_regional.py reads geography for each city but cannot move its position) vs. re-derived from attractors (generator places cities from geography, uses markers.json for population counts only).
- **L2-Q5:** TerritorialStatus quantitative derivation thresholds — what are the concrete numbers for CoreTerritory vs. FrontierTerritory vs. ContestZone?
- **L2-Q6 / OQ-R4-B1:** Latent settlement active/ghost flag storage — in a systems.db Phase 3 output table (recommended) vs. runtime ECS component vs. purely computed from current sim state.
**Must be resolved before Layer 3:**
- **L3-Q1:** FoundingOrientation spatial grid orientation — does it modify district placement grid positions (spatial rotation), or only modify the prosperity gradient direction?
- **L3-Q2:** Political archetype as spatial arrangement — explicit spine/center/nodes patterns, or emergent from weight table?
**Must be resolved before Layer 4:**
- **L4-Q1 / OQ-R4-G2:** Economics-variable rendering mechanism — bake at generation + update on event; compute live from `prosperity_current` per frame; or cache per chunk, invalidate on threshold crossing.
- **CL-Q2 / OQ-R4-B2:** Regional land-use evolution resolution boundary — runtime `regional_land_use` column update vs. ChunkMutations.
- **L4-Q6 / CL-Q1:** `prosperity_delta` two-field architecture — formal adoption as named architectural decision.
---
## The Output: `docs/workshops/planet-down-cascade/BRIEF.md`
**The primary deliverable of the generation-cascade workshop is a new workshop brief.**
`docs/workshops/planet-down-cascade/BRIEF.md` was assembled by Tyre from Round 4 material. It contains:
1. **The Lead's Cascade Vision** — reproduced verbatim as authoritative framing
2. **Given Facts** — all decisions from Rounds 1-4 that are locked, including: pipeline architecture, seed chain, CityGenerationContext struct, city decomposition formula, Phase 1 minimum scope, WorldTier enum, GeneratorChunkData upgrade, 6-field read set, 10×9 weight table, sub-settlement hierarchy, latent settlement principle, determinism boundary per layer, LoD cascade
3. **Four workshop sections** (Layer 1 through Layer 4) each with specific open questions (L1-Q1 through L4-Q6)
4. **Cross-layer questions** (CL-Q1 through CL-Q4)
5. **Workshop format** — 3 rounds: Round 1 inventory and framing, Round 2 algorithm proposals, Round 3 convergence and D-records
6. **Required reading list** — round-3 notes, tyre-round3.md, existing server code files, decisions/architecture.md, Round 4 domain files
7. **Expected outputs** — algorithm specifications, data format handoffs, D-records, implementation ticket dependency chain, resolved open questions
---
## How the Workshop Evolved
The generation-cascade workshop did not end where it started. The journey:
1. **Started as:** A 3-round implementation audit with a city-outward framing — trace the generation layers a walkable apartment would need
2. **After Round 1:** Scope narrowed (heritage grammar / NPC / #615 removed); participant list adjusted; city-outward framing retained
3. **After Round 2:** Phase 3 corrective — the workshop had been designing Phase 5 without designing Phase 3; `generate_regional.py` framing introduced
4. **After Round 3:** Four unresolved lead decisions; planet-down reframe needed; a fourth round launched to gather requirements for a new brief
5. **After Round 4:** All Round 3 decisions resolved; governing rules (Determinism + LoD) named; new workshop brief assembled; workshop closed
The city-outward framing produced valid technical work (Rounds 1-2: all D-C1 through D-C8 candidates, effort estimates, implementation order). The Phase 3 corrective (Round 3) caught the missing layer. The planet-down reframe (Round 4) gave the correct causal ordering that makes the full cascade coherent.
The workshop did not produce implementation — it produced a brief that will enable the implementation workshop to proceed without foundational architecture questions.
---
*Document compiled by Qatux from all four round sources. This is the authoritative record of what happened in the generation-cascade workshop. For technical details, see round-1-notes.md through round-4-notes.md. For the output brief, see docs/workshops/planet-down-cascade/BRIEF.md.*