Planet-down cascade workshop (3 rounds, 5 agents): layer-by-layer generation from empty world through population overlay, city planning, and street rendering. Includes consultant review by Troblum. Also commits: pre-Sprint-35 DB backup, Claude Code team-mode tmux test log (team-test.md). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
42 KiB
title, description, type, status, workshop, agent, round, created
| title | description | type | status | workshop | agent | round | created |
|---|---|---|---|---|---|---|---|
| Paula — Round 3: Convergence and Narrative D-Records | Convergence positions on Province boundary legibility, founding_age → layout_mode, and mismatch flagging; D-record candidates for all narrative/political decisions locked in this workshop; narrative consistency review of other participants' proposals | workshop | active | planet-down-cascade | paula | 3 | 2026-05-01 |
Planet-Down Cascade — Round 3 (Paula)
Focus: Convergence and D-record candidates. Three convergence items from Round 2 notes; six D-record candidates for narrative/political decisions; narrative consistency review of other participants' proposals.
Lead decision absorbed: Two-tier mismatch flagging — score < 0.35 = warning, score < 0.15 = error. This resolves Q1 from Round 2 notes.
Convergence Items
Item 1: Province Boundary Legibility (Q3)
Ozzie's requirement: Province boundaries must be visible on the planetary map as natural features (watershed lines, drainage basins), not arbitrary grid lines. If Province boundaries aren't legible, TerritorialStatus doesn't communicate to the player.
Narrative position: adopt this requirement fully.
Province boundaries ARE drainage basin boundaries — this is how they are defined in the spatial hierarchy (Amendment 4: Province = "road-map travel region defined by natural boundaries from Layer 1: drainage basins, ridgelines"). The Layer 1 river network at Province resolution already defines these boundaries as geographic facts. The planetary map does not need to render a separate "Province boundary" layer — it needs to render the coarse river and ridgeline network, and Province boundaries will emerge naturally from that rendering.
The correct rendering chain:
- Layer 1 produces a coarse river network at Province resolution (drainage basin edges)
- The planetary atlas UI renders this river network as thin geographic lines
- Province boundaries are the visible boundaries between drainage basins — rendered by the river network, not as political overlays
- TerritorialStatus is a color or texture fill within each Province's geographic boundary (the space between the river lines)
What the player sees: The political map IS the geographic map. The watershed lines are both rivers and Province boundaries. This is historically accurate — rivers have always been the primary political boundaries because they are obvious, defensible, and non-arbitrary. A player looking at the atlas UI reads the river as a boundary without being told it's a boundary.
Implementation note (not my domain, but relevant): The Province-level river network needs to be stored in BodyWorldState as a renderable set of line segments (not just as a list of confluence points). This is an additional field in the Layer 1 output that the atlas UI consumes. Tyre should confirm this is included in RiverNetwork in BodyWorldState.
D-record implication: The TerritorialStatus D-record candidate below includes this rendering specification as part of the decision — it is not a separate UI decision, it is integral to how TerritorialStatus is designed to communicate.
Item 2: founding_age → layout_mode Narrative Assessment (Q2)
Ozzie's proposal: founding age should modify layout_mode as well as prosperity_baseline. Old settlements → irregular layout modes. Young settlements → grid layout.
Narrative assessment: adopt with the following constraints.
The historical basis is correct and strong: planned settlements are grids (the company built this to a specification); grown settlements are organic (no one planned the whole thing, it accumulated). This is one of the most legible signals in real-world urban history — you can look at a city and tell whether it was planned or grew.
The narrative complication is that political_archetype already governs the spatial arrangement pattern, and some archetypes are intrinsically planned (CompanyTown, AdminCapital) while others are intrinsically organic (OrganicGrowth). Age should interact with the pattern, not override it.
My proposed interaction rules:
CompanyTown (spine pattern):
Always grid. Age does not introduce irregularity — the company planned it and
maintains that plan. Age instead produces: character tags (legacy_infrastructure,
retrofitted) and prosperity gradient aging effects. The SPINE gets more legible
with age (maintenance history visible), not more organic.
AdminCapital (radial pattern):
Core (Administrative district) always planned and maintained.
Outer rings accumulate irregularity with age:
layout_irregularity = clamp(founding_age_years / 500.0, 0.0, 0.6)
Result: ancient AdminCapitals have planned cores and organic peripheries.
This is historically very accurate (Roman city center, medieval accretion around it).
FreePort (multi-node pattern):
Node centers remain planned (commerce is maintained by self-interest).
Between-node fill becomes increasingly organic with age.
layout_irregularity = clamp(founding_age_years / 300.0, 0.0, 0.8)
Older FreePorts feel like the overlay of many eras.
Contested (dual-center overlay):
Each faction's half uses its own archetype's age rules.
The contested boundary zone accumulates irregularity at double rate:
boundary_irregularity = clamp(founding_age_years / 200.0, 0.0, 1.0)
Very old contested cities have incoherent seam zones.
OrganicGrowth (irregular local density):
Already maximally irregular. Age has no additional effect on layout_mode.
Age instead deepens the character modifiers (more legacy_infrastructure,
more layers visible).
Minimum viable implementation: layout_irregularity: f32 as an additional field on each district (set by Layer 3 generation, derived from archetype + age interaction rules above). The block generation algorithm at Layer 4 reads this value to vary block shape regularity and street angle consistency. Value 0.0 = perfect grid; value 1.0 = fully organic street angles.
This is my accepted position. I have no blocking objection to Ozzie's proposal — I'm specifying how it interacts with the five spatial arrangement patterns.
Item 3: Mismatch Flagging Integration with Name Reservation System
Lead decision: score < 0.35 = warning, score < 0.15 = error.
Integration with the Stage 2 fulfillment pipeline:
During attractor-matching (Stage 2):
For each named city reservation (in priority order):
1. Compute best available score for this reservation
2. Assign to best available attractor regardless of score
3. Classify the assignment:
- score ≥ 0.35: MATCH — no flag
- score 0.15–0.35: ATTRACTOR_WARNING — write to generation_log
- score < 0.15: ATTRACTOR_ERROR — write to generation_log
For corp HQ reservations specifically (Tier A):
- Elevate one tier: WARNING → ERROR if corp_id is referenced in active
trade routes or supply chains in systems.db. A misplaced HQ with
active trade dependencies is a more serious data quality problem
than a misplaced unnamed city.
Generation log entry format:
{
level: "WARNING" | "ERROR",
body_id: i64,
reservation_name: String,
placed_at: GeographicFeatureTag | "Synthetic",
compatibility_score: f32,
corp_id: Option<i64>,
available_compatible_attractors: u32, // were any compatible attractors available?
recommendation: &str // e.g. "review economic_role in atlas_city_names"
}
The generation_log in BodyWorldState is a Vec<GenerationLogEntry>. It persists for the session. Developer tooling should be able to dump this for quality review. This is not surfaced to the player.
Q5 resolution (how city names enter atlas_city_names): The population path is: wiki content (Miri's authored city/settlement lists in wiki articles) → import_economics.py reads wiki content and populates atlas_city_names at build time. The generator does not invent names; it fulfills name reservations from the wiki. This matches the existing pipeline pattern: import_economics.py already reads wiki TOML files for brands and economics data. City names are the same pattern.
Implication: atlas_city_names requires a corresponding wiki content pass before any body can have named cities. Bodies without wiki city content will have 0 name reservations — the generator places unnamed settlements at all attractors and assigns generated names from the cultural pool. This is acceptable for frontier/minor bodies; it is a content gap for major bodies.
D-Record Candidates
These candidates require formal D-NNN IDs, which must be claimed via tooling/db/decision claim before filing. The candidates below are ready to claim.
D-C-TS: TerritorialStatus — Enum and Derivation Algorithm
Decision statement: TerritorialStatus is a six-value enum derived per Province as a post-processing pass after Layer 2 settlement placement and road graph generation. It is runtime-updated when active settlement count changes.
Rationale: TerritorialStatus makes the political character of traversable space between major settlements legible from the planetary atlas UI and from street-level approach conditions (road quality, signage, lighting density). It cannot be authored without also authoring all settlement placement; it cannot be purely emergent at runtime without a generation-time anchor. The Layer 2 post-processing pass produces it once at generation time, and the economic sim updates it when settlement activity changes.
Enum specification:
pub enum TerritorialStatus {
CoreTerritory, // Established; maintained road infrastructure throughout Province
FrontierTerritory, // Active settlement; infrastructure thin at Province edges
ExtractiveZone, // Resource extraction dominant; corporate road investment visible
ContestZone, // Two or more competing political actors; infrastructure incoherent
WildernessBuffer, // Never settled; no settlement placed at Layer 2
AbandonedZone, // Was settled (placed_at_generation = true); now inactive
}
Derivation algorithm:
fn derive_territorial_status(province: &ProvinceWorldState) -> TerritorialStatus {
// AbandonedZone: was settled, now empty. Checked first — runtime override.
if province.placed_at_generation && !province.active {
return TerritorialStatus::AbandonedZone;
}
// WildernessBuffer: was never settled.
if !province.placed_at_generation {
return TerritorialStatus::WildernessBuffer;
}
// ExtractiveZone: resource extraction + corporate presence + characteristic roads.
if province.primary_economic_activity == EconomicActivity::Extraction
&& province.corporate_presence_score > 0.4
&& province.extraction_corridor_quality > 0.60
&& province.off_corridor_road_quality < 0.35 {
return TerritorialStatus::ExtractiveZone;
}
// ContestZone: overlapping political authority.
if province.jurisdiction_overlap_score > 0.3 {
return TerritorialStatus::ContestZone;
}
// CoreTerritory: well-connected, maintained infrastructure.
if province.infrastructure_quality > 0.6
&& province.corporate_road_maintenance > 0.5 {
return TerritorialStatus::CoreTerritory;
}
// FrontierTerritory: active but underdeveloped. Catch-all.
TerritorialStatus::FrontierTerritory
}
Key architectural requirement: province.placed_at_generation: bool is an immutable flag set at Layer 2 generation time and never modified. It is the only runtime-available signal distinguishing WildernessBuffer (never settled) from AbandonedZone (settled, now empty). province.active: bool is the runtime-variable field.
Province boundary rendering: Province boundaries are rendered on the planetary atlas UI as the coarse river and ridgeline network from Layer 1 (drainage basin edges). TerritorialStatus is a fill color/texture within each Province's geographic boundary. Province boundaries are geographic facts (watershed boundaries), not political overlay lines.
Derivation trigger: TerritorialStatus is re-derived per Province when active_settlement_count for the Province changes. The economic sim owns this trigger.
Blocks: Layer 2 road graph + settlement placement must be complete before TerritorialStatus derivation runs.
D-C-FO: FoundingOrientation — Enum and Derivation
Decision statement: FoundingOrientation is a five-value enum directly readable from the AttractorAssignment of the settlement at Layer 2 generation time. No authoring pass required for the common case; named cities may have an override column for setting lore that explicitly contradicts geographic logic.
Rationale: In the planet-down cascade, the geographic feature that caused a settlement to exist is known at generation time (it is the assigned attractor). FoundingOrientation is a direct lookup from that feature. This eliminates the sanity-check authoring pass that would otherwise be required and makes the field deterministic from generation-time data.
Enum specification:
pub enum FoundingOrientation {
PortFacing, // Settlement grew around a harbor or river confluence
RailHeadFacing, // Settlement grew around a transit/rail network junction
ResourceFacing, // Settlement grew around an extraction resource site
DefenseFacing, // Settlement grew around defensible terrain
AdminFacing, // Settlement exists by political decision; no geographic trigger
}
Derivation lookup:
AttractorAssignment::Geographic(CoastalHarbor | RiverConfluence) → PortFacing
AttractorAssignment::Geographic(ResourceConcentration) → ResourceFacing
AttractorAssignment::Geographic(MountainPass | Defensible) → DefenseFacing
AttractorAssignment::Geographic(ArablePlain) → AdminFacing
(agricultural plain settlements are administrative service hubs, not resource/port/defense cities)
AttractorAssignment::Synthetic(_) → AdminFacing
RailHeadFacing special case: This value is not derivable from a geographic attractor (transit junctions are infrastructure, not terrain features). RailHeadFacing is assigned in a second pass after road/rail graph generation, when the road algorithm identifies a settlement that sits at a road/rail network junction of high connectivity. It overrides the initial AdminFacing from Synthetic assignment when the topology warrants it.
Named city override: atlas_city_names may include a founding_orientation_override column (nullable). Non-null values override the derivation lookup. This is the authoring escape hatch for setting lore that explicitly places an administrative capital at what would otherwise read as a resource site (e.g., a corporate headquarters that was also a founding city, built at a resource site but now a genuine capital).
Storage: In CityGenerationContext as founding_orientation: FoundingOrientation, passed from Layer 2 to Layer 3 for district grid orientation.
D-C-AA: AttractorAssignment — OrganicGrowth Disambiguation Mechanism
Decision statement: Every GeneratedSettlement carries an AttractorAssignment enum that records whether the settlement was placed at a geographic attractor or at a synthetic position. This is the mechanism that makes FoundingOrientation derivation unambiguous and enables OrganicGrowth political_archetype identification.
Rationale: Without this field, OrganicGrowth settlements (excess population, no attractor) and AdminFacing settlements (politically placed at a Province centroid) are algorithmically indistinguishable — both have no geographic trigger. The enum makes the distinction explicit in the generation record.
Enum specification:
pub enum AttractorAssignment {
Geographic {
attractor_type: GeographicFeatureTag,
quality_score: f32, // 0.0–1.0; scores below 0.35 generate a warning
},
Synthetic {
reason: SyntheticPlacementReason,
},
}
pub enum SyntheticPlacementReason {
PopulationOverflow, // More named cities needed than geographic attractors available
PoliticalDecision, // systems.db marks this city as politically placed
CorpExpansion, // High economic_tier body; settlement density exceeds attractor supply
}
OrganicGrowth political_archetype derivation:
A settlement is assigned PoliticalArchetype::OrganicGrowth when:
attractor_assignment == Synthetic(PopulationOverflow)
AND no dominant corp presence (corp_presence_count == 0 or no single corp > 40% presence)
AND economic_role ∈ {service_mixed, commercial}
Otherwise, synthetic settlements derive political_archetype from economic_role + corp_presence as normal.
Storage: Field on GeneratedSettlement in BodyWorldState. Part of the session-reproducible Layer 2 output.
Interaction with SettlementClass: Burnelli-Sheldon's SettlementClass::OrganicGrowth value corresponds to AttractorAssignment::Synthetic(PopulationOverflow) + the political_archetype derivation above. These are consistent descriptions at different levels of the architecture. Round 3 alignment note: the OrganicGrowth name should appear in both enums for consistency — SettlementClass::OrganicGrowth and PoliticalArchetype::OrganicGrowth — with AttractorAssignment::Synthetic(PopulationOverflow) as the underlying mechanism that produces both.
D-C-SA: Five Spatial Arrangement Patterns
Decision statement: All five political archetypes have explicit, mandatory spatial arrangement patterns for Layer 3 district grid placement. No archetype uses the weight table alone to determine spatial arrangement. The weight table (three-component model) determines district TYPE mix; the spatial arrangement pattern determines district POSITION.
Rationale: The weight table cannot produce CompanyTown spine topology, AdminCapital radial structure, or Contested dual-center overlap. Spatial arrangement requires explicit algorithms. Without explicit patterns, the generator produces cities that are distinguishable by district type counts but not by geometry — the player cannot identify the archetype from walking around. Ozzie's acceptance criterion (15-second identification) requires geometry, not statistics.
Acceptance criterion: A player unfamiliar with the archetype system should be able to identify the city's political character from 15 seconds of walking in any district, based on street layout alone.
The five patterns:
Pattern 1 — CompanyTown (Spine):
Governing input: founding_orientation (sets spine axis direction)
District sequence along spine axis:
Position 0 (resource-facing end): Industrial or LogisticsHub
Position 1: Mixed (rough services)
Positions 2..N-2: Residential (gradient from denser near industrial to more spacious far end)
Position N-1 (far end): Commercial or Administrative
Spine width: 1 district for N≤4; 2 districts for N>4
layout_irregularity: 0.0 (planned grid, increases with age per founding_age rules)
prosperity_baseline gradient: increases from resource end toward far end (+0.10 across full length)
Pattern 2 — AdminCapital (Radial):
Governing input: founding_orientation
DefenseFacing variant:
Administrative district at geometric center of district grid
Adjacent ring: Commercial, Entertainment, Mixed
Outer ring: Residential, Industrial, Logistics
Prosperity gradient: decreases radially from center
AdminFacing variant:
Administrative district at edge facing primary road approach direction
(primary road approach = highest-quality road in road_entry_directions)
Middle rows: Residential, Commercial, Entertainment
Back row (away from prestige edge): Industrial, Logistics
Prosperity gradient: decreases from prestige row toward back row
layout_irregularity: 0.0 at core; increases at outer rings with age (see founding_age rules)
Pattern 3 — FreePort (Multi-Node):
Node count: 2 for N≤6; 3 for N>6
Node 0: FoundingOrientation edge (primary harbor/confluence)
Node 1: [30–50%] of district_grid_width along geographic feature axis from Node 0
Node 2 (if present): seed-derived position ~opposite Node 0
District allocation:
Node 0: ceil(N × 0.45) districts
Node 1: ceil(N × 0.35) districts
Node 2 (if present): remainder
Within each node:
Node center: Commercial or Entertainment
Adjacent to center: Mixed
Node periphery: Residential
layout_irregularity: 0.0 at node centers; increases between nodes with age
Prosperity gradient: local per node, no city-wide gradient
Pattern 4 — Contested (Dual-Center Overlay):
Contested boundary derivation (priority order):
1. NaturalBarrier tag bisecting city footprint → use barrier line
2. Road axis perpendicular bisector through city center
3. Seed-derived offset: boundary_offset = (child_seed(city_seed, BOUNDARY) % 25 - 12)%
(displacement of ±12% from center, preventing exact bisection)
Faction A (larger corp cluster or corporate faction):
Occupies half of district grid facing FoundingOrientation direction
Arranged by Faction A's inferred archetype (spine, radial, or service grid)
Faction B (smaller corp cluster or administrative faction):
Occupies opposite half
Arranged by Faction B's inferred archetype (different from Faction A)
Boundary-adjacent districts (1-district buffer on each side of boundary line):
DistrictType: Mixed
perimeter_treatment: Checkpoint (both active authorities) or Walled (one retreated)
prosperity_baseline: district_base - 0.15 (contested areas suppressed)
layout_irregularity: each half follows its archetype's rules independently;
boundary zone always layout_irregularity = 0.7+ (contested areas are architecturally incoherent)
Prosperity gradient: increases away from boundary toward each faction's center
Pattern 5 — OrganicGrowth (Irregular Local Density):
Local attractor points (seed-derived within city footprint):
Attractor 0: geometric center (always present)
Attractor 1: seed-derived offset [20–40%] of district_grid_width from center
Attractor 2 (for N≥6): ~opposite Attractor 1
Voronoi partition: each district assigned to nearest attractor
Within each attractor cluster:
Attractor-center district: Commercial or Mixed
Adjacent districts: Residential, Entertainment, Commercial (seed-determined sequence)
Grid jitter per district:
offset_x = (child_seed(district_seed, JITTER_X) % (DISTRICT_WIDTH / 4)) - (DISTRICT_WIDTH / 8)
offset_y = (child_seed(district_seed, JITTER_Y) % (DISTRICT_WIDTH / 4)) - (DISTRICT_WIDTH / 8)
(±12.5% of district width; breaks regularity without requiring collision detection)
layout_irregularity: fixed at 0.8 (always highly organic; age has no additional effect)
Prosperity gradient: per attractor cluster, local; no city-wide gradient
Three-component model compatibility: The population tier guarantees from Burnelli-Sheldon's three-component model apply within each pattern. All five patterns must accommodate the mandatory district types for the settlement's population tier. The patterns position mandatory districts; the district mix algorithm assigns counts. Where the pattern specifies a position type (e.g., "Position 0: Industrial or LogisticsHub"), mandatory district guarantees take precedence over the position type if the guaranteed types do not include Industrial/Logistics at this population tier.
D-C-NR: Name Reservation System
Decision statement: City and geographic feature identity is stored in two new systems.db tables (atlas_city_names, atlas_feature_names) populated at build time from wiki content. The runtime generator reads these as name reservations and fulfills them during Layer 1-2 generation. Name assignment follows a four-stage pipeline.
Rationale: Amendment 3 eliminated authored city positions. Named cities that appear in corporate records, faction histories, and player-facing content must still exist and be of the appropriate type. Name reservations (not positions) are the mechanism that guarantees corporate cross-references resolve while allowing positions to be fully generated.
Table schemas:
CREATE TABLE IF NOT EXISTS atlas_city_names (
id INTEGER PRIMARY KEY,
body_id INTEGER NOT NULL REFERENCES bodies(id),
name TEXT NOT NULL,
population INTEGER NOT NULL,
economic_role TEXT NOT NULL, -- matches EconomicRole enum
corp_id INTEGER REFERENCES corporations(id), -- nullable; corp HQ
tier_hint INTEGER, -- nullable; expected WorldTier
founding_orientation_override TEXT, -- nullable; overrides derivation lookup
UNIQUE(body_id, name)
);
CREATE TABLE IF NOT EXISTS atlas_feature_names (
id INTEGER PRIMARY KEY,
body_id INTEGER NOT NULL REFERENCES bodies(id),
name TEXT NOT NULL,
feature_class TEXT NOT NULL, -- 'river' | 'mountain_range' | 'ocean' | 'sea' | 'bay'
rank_hint INTEGER, -- 1 = primary/largest, 2 = secondary; NULL = seed-assigned
UNIQUE(body_id, name)
);
Population path: import_economics.py reads wiki content (Miri's authored settlement and geographic feature lists in wiki articles) and populates both tables at build time. Bodies without wiki coverage produce 0 name reservations; the generator places unnamed settlements for all attractors and assigns generated names from the cultural pool.
Four-stage fulfillment pipeline:
Stage 0 (build-time): Populate atlas_city_names + atlas_feature_names from wiki.
Mark HQ-constrained names (corp_id IS NOT NULL) for priority handling.
Stage 1 (Layer 1 runtime — during feature tag extraction):
For each entry in atlas_feature_names for this body:
Match to the generated feature of that class with matching rank_hint.
rank_hint = 1 → largest/longest feature of that class (by flow volume for rivers,
peak elevation for mountain ranges).
rank_hint = NULL → seed-derived rank assignment.
Store: (body_id, feature_name, generated_feature_id) in BodyWorldState.
Stage 2 (Layer 2 runtime — during attractor matching):
Sort atlas_city_names entries:
Priority 1: entries with corp_id IS NOT NULL (HQ-constrained; Tier A assignment)
Priority 2: remaining entries, sorted by population DESC
For each entry in sorted order:
Compute compatibility score against available attractors
Assign to highest-scoring attractor
Classify: MATCH (≥0.35) | WARNING (0.15–0.35) | ERROR (<0.15)
Elevate WARNING → ERROR for corp_id IS NOT NULL entries with active trade routes
Write mismatch entries to BodyWorldState.generation_log
Stage 3 (Layer 2 runtime — overflow and unnamed):
Remaining population quota settlements: assign generated names from cultural pool.
Cultural pool selection: cultural_hint from nearest fulfilled named reservation,
or body's primary biome class as fallback.
Mismatch log format:
pub struct GenerationLogEntry {
pub level: LogLevel, // Warning | Error
pub body_id: i64,
pub reservation_name: String,
pub placed_at: String, // attractor_type or "Synthetic"
pub quality_score: f32,
pub corp_id: Option<i64>,
pub available_compatible_count: u32,
pub recommendation: String,
}
Relationship to atlas_cities: atlas_cities currently stores authored positions. After Amendment 3, positions are generated at runtime; the table is repurposed for static geographic reference data (atlas pixel coordinates for other authoring purposes) or deprecated. atlas_city_names is the name reservation system, not a position store.
D-C-AM: Attractor-Matching Narrative Constraints
Decision statement: The attractor-matching algorithm applies narrative plausibility constraints in two tiers: physical impossibility checks (hard zeros before scoring) and narrative penalty/bonus scoring adjustments. These constraints layer on top of Burnelli-Sheldon's objective function without replacing it.
Rationale: The scoring function maximizes aggregate geographic plausibility. Without narrative constraints, the algorithm could maximize scores while still producing individual placements that are physically or narratively impossible (a fishing port in mountains; a research hub at a corporate extraction site). Narrative constraints prevent these edge cases from occurring silently.
Physical impossibility check (is_physically_possible):
Run before score matrix population. Returns false → score = 0.0 (settlement cannot be placed at this attractor under any circumstances):
fn is_physically_possible(role: EconomicRole, attractor: GeographicFeatureTag,
body: &BodyData) -> bool {
match (role, attractor) {
// Agricultural settlement on a mountain pass is physically impossible
(Agricultural, MountainPass) => false,
// Extraction settlement on arable plain without subsurface resources is impossible
(Extraction | Mining, ArablePlain) if !body.has_subsurface_resources => false,
// Port city on inland terrain is impossible
// (CoastalHarbor and RiverConfluence already imply coast/river; no inland attractor
// of these types can exist — this check is a safety net)
_ => true,
}
}
Hard constraint rules (H1–H4, applied as score → 0.0 overrides):
H1 (Maritime incompatibility): Corp with primary_operation = maritime_logistics or
fishing cannot be assigned to MountainPass or Defensible attractors.
H2 (Resource-type mismatch): Corp HQ with economic_role = extraction assigned to
a ResourceConcentration attractor where the resource type does not match the
corp's commodity_type → score = 0.0. (A lithium mining corp cannot be placed
at an oil extraction concentration.)
H3 (Research isolation): Corp HQ with economic_role = research cannot be assigned
to CoastalHarbor unless body's planet_class includes aquatic/oceanic research
biome tag.
H4 (Administrative terrain): Settlement with economic_role ∈ {service_mixed} at
economic_tier ≥ 3 (high administrative density) cannot be assigned to MountainPass
as its sole compatible attractor. Administrative power requires accessible terrain.
Soft constraint scoring adjustments (additive to Burnelli-Sheldon's matrix):
| economic_role | Attractor | Narrative adjustment |
|---|---|---|
| agricultural | MountainPass, ResourceConcentration | −0.40 |
| agricultural | ArablePlain | +0.40 |
| commercial | ResourceConcentration, Defensible | −0.25 |
| commercial | RiverConfluence, CoastalHarbor | +0.35 |
| manufacturing | MountainPass, Defensible | −0.20 |
| manufacturing | CoastalHarbor, ArablePlain | +0.20 |
| extraction, mining | ArablePlain (no subsurface) | −0.45 |
| extraction, mining | ResourceConcentration | +0.50 |
| transit | ArablePlain, Defensible | −0.15 |
| transit | RiverConfluence, CoastalHarbor | +0.35 |
| frontier | RiverConfluence, CoastalHarbor | −0.10 |
| frontier | MountainPass, ResourceConcentration | +0.25 |
| research | RiverConfluence (primary) | −0.15 |
| research | Defensible, ArablePlain | +0.15 |
| energy | ArablePlain (no resource tag) | −0.30 |
| energy | ResourceConcentration | +0.30 |
Mismatch classification (post-assignment):
quality_score ≥ 0.35: MATCH (no log entry)
quality_score 0.15–0.35: WARNING (ATTRACTOR_WARNING log entry)
quality_score < 0.15: ERROR (ATTRACTOR_ERROR log entry)
Corp HQ mismatches: elevate one tier (WARNING→ERROR if active trade routes present)
D-C-NL: Naming Registers — Lookup Table
Decision statement: District and settlement names are selected from named registers via a lookup table: (DistrictType, WorldTier) → register. Name pool population (the actual strings) is deferred content work; the register selection architecture is locked.
Rationale: The naming architecture must be locked before Phase 3 map content can be generated, because district names and street names require pools to draw from. The pools are authored content; the lookup table is an architectural decision. These can proceed in parallel once the lookup table is locked.
Register definitions:
Geographic: Names derived from terrain features (river names, hill names, bay names)
Used for: pre-settlement features; settlements at geographic junctions
Functional: Names derived from what the place does (The Exchange, Processing Ward)
Used for: industrial districts; extraction sites; transit nodes
Commemorative: Names derived from persons or events (founders, battles, corporations)
Used for: administrative districts; capital cities; corp-named towns
Aspirational: Names derived from ideals or future-orientation (New [X], Unity [X])
Used for: planned new districts; FreePort commercial nodes; frontier settlements
Folk: Names derived from local practice or informal usage (Old Town, The Flats)
Used for: organic districts; old residential areas; sub-settlements
Corporate: Names derived from the controlling corporation (Corp-ward, Company Row)
Used for: CompanyTown districts; corp extraction sites
Dual: Two names (official + common), one from each of two competing registers
Used for: ContestZone districts; politically disputed settlements
Register lookup table by (DistrictType, WorldTier):
| DistrictType | Epicenter/Regional | Backwater | Passage | Waypoint |
|---|---|---|---|---|
| Residential | Folk / Commemorative | Folk | Folk | Folk |
| Commercial | Functional / Aspirational | Functional | Functional | Functional |
| Industrial | Functional / Corporate | Functional | Functional | Functional |
| Administrative | Commemorative | Commemorative | Commemorative | Functional |
| Entertainment | Aspirational / Folk | Folk | Folk | Folk |
| Mixed | Folk | Folk | Folk | Folk |
| Logistics | Functional | Functional | Functional | Functional |
| Transit | Functional / Geographic | Functional | Functional | Functional |
| Civic | Commemorative / Aspirational | Commemorative | Aspirational | — |
Political archetype modifier (overrides default register):
CompanyTown: All districts → Functional or Corporate (company names its infrastructure)
Exception: Residential districts → Folk (workers name their own neighborhoods)
AdminCapital: Administrative district → Commemorative (always)
Other districts → preserve WorldTier defaults
FreePort: Commercial districts → Aspirational (commerce names for what it offers)
Entertainment districts → Aspirational or Folk
Administrative districts → Functional (admin is a utility, not a monument)
Contested: All districts in contested boundary zone → Dual register
Each faction's half → its own archetype's register
OrganicGrowth: All districts → Folk (no authority named anything)
Naming layers by cascade level:
Layer 1 (Empty World):
Geographic register names only.
Major terrain features (river systems, mountain ranges) large enough for atlas labeling
receive pre-political geographic register names as placeholders.
Source: atlas_feature_names table.
Layer 2 (Population Overlay):
First human-assigned names at settlement creation.
Register derived from founding trigger:
RiverConfluence/CoastalHarbor → Geographic (the confluence was named before the city)
ResourceConcentration + corp_presence → Corporate or Functional (resource names the place)
Defensible/MountainPass + AdminCapital archetype → Commemorative
ArablePlain agricultural cluster → Geographic or Folk
Synthetic(PoliticalDecision) → Commemorative
Geographic feature placeholder names from Layer 1 may persist as the "common name"
while Layer 2 administrative name becomes official → DualNaming origin.
Layer 3 (City-Level Planning):
District names from lookup table above.
Political archetype modifier applied.
Layer 4 (Street-Level Rendering):
Working street names (pragmatic; what inhabitants call the street).
These may differ from official planning names in:
- ContestZone cities (each faction has different official names)
- Old cities (street was renamed by new authority; old name persists in use)
- OrganicGrowth cities (streets often have no official names; working names only)
Name pool population: This decision locks the register selection architecture. The actual string pools for each register are content work (Mellanie's domain). The generator needs only the pool selection rule (which register) and pool reference (which cultural pool for that register). Pool availability is a content dependency, not an architecture dependency.
Narrative Consistency Review
Burnelli-Sheldon's integer multiplier table
Minor flag — Agricultural Commercial weight: Agricultural economic_role has Commercial weight = 18. This seems high. A large agricultural city's commercial infrastructure is primarily for farming supply and produce trade — it's functional commerce, not a dense urban commercial district. 12-14 would feel more plausible without eliminating Commercial presence. Not a blocking concern; the three-component model's guaranteed minimum (Town tier includes Commercial) handles small settlements, and the 18 weight at City scale won't produce implausible results.
Flag — Contested archetype modifier missing: The political archetype weight modifiers table includes CompanyTown, AdminCapital, and FreePort. Contested is absent. What modifiers apply to Contested political_archetype? My Round 2 document specifies the Contested spatial pattern has suppressed prosperity at the contested boundary and mixed district types there — but the district type WEIGHT for Contested cities overall is undefined. Proposed Contested modifiers: Mixed +15, Administrative −10 (each faction's administration is partial; Mixed is the compromise). Burnelli-Sheldon should confirm or amend.
Minor flag — Political archetype modifier floor interaction: FreePort modifier on Administrative: −20. For Transit/Port economic_role, Administrative base weight = 7. After −20 modifier: 7 − 20 = −13 (below zero). The minimum floor of 3 must be applied after all modifiers, not before. Confirm implementation applies floor after all modifiers rather than clamping each modifier independently.
Tyre's atlas_city_names schema (ARCH-3)
Tyre's locked schema:
CREATE TABLE atlas_city_names (
id INTEGER PRIMARY KEY,
body_id INTEGER NOT NULL REFERENCES bodies(id),
name TEXT NOT NULL,
corp_id INTEGER REFERENCES corporations(id),
tier_hint INTEGER,
reserved BOOLEAN NOT NULL DEFAULT 0
);
Flag — Missing population and economic_role columns:
My D-C-NR requires population and economic_role in atlas_city_names for:
- Population: needed to sort named cities for attractor assignment priority (largest first within Tier B/C)
- economic_role: needed for attractor compatibility scoring (hard constraints H1-H4 + soft constraint table)
Without these columns, the name fulfillment pipeline cannot determine assignment priority or check compatibility. Proposed resolution: add both columns to Tyre's ARCH-3 schema. These values are available from wiki content at import_economics.py build time.
Flag — reserved column purpose unclear:
Tyre's schema includes reserved BOOLEAN NOT NULL DEFAULT 0. The purpose is not specified in Round 2 notes. If this means "HQ-constrained reservation" (equivalent to corp_id IS NOT NULL), it is redundant with the corp_id column. If it means something else, clarify.
Gestalt's DistrictSkeleton layout_mode
Gestalt's layout_mode is currently a fixed archetype value. Ozzie's proposal (founding_age → layout_mode) would add layout_irregularity: f32 as an additional field. This does not conflict with the existing layout_mode — layout_irregularity is a modifier on the layout, not a replacement for layout_mode. Proposed resolution: layout_mode stays as is (fixed archetype); layout_irregularity: f32 is added as a separate field. These are orthogonal: layout_mode says WHAT pattern; layout_irregularity says HOW regular. I'm adopting this in the D-C-SA entry (founding_age rules per archetype).
SettlementClass vs. AttractorAssignment terminology
Burnelli-Sheldon's SettlementClass::OrganicGrowth and my PoliticalArchetype::OrganicGrowth and my SyntheticPlacementReason::PopulationOverflow all describe the same condition from different angles. These need consistent naming. Proposed alignment:
SettlementClass::OrganicGrowth corresponds to:
AttractorAssignment::Synthetic(PopulationOverflow) [mechanism]
+ PoliticalArchetype::OrganicGrowth [political classification]
+ FoundingOrientation::AdminFacing [spatial consequence]
All three are consistent descriptions. The OrganicGrowth name should appear at the SettlementClass level and the PoliticalArchetype level; PopulationOverflow is the mechanism that produces it.
Open Items After Round 3
For lead resolution
Contested archetype weight modifier: Not specified by Burnelli-Sheldon. Proposed: Mixed +15, Administrative −10. Awaiting confirmation.
L3-Q7 (port/station direction) — final disposition: Mechanism is designed (one-query, one-lookup, one-edge-assignment). Ozzie wants implementation. Gestalt and Tyre both classified it as deferred. The mechanism is simple enough that it can be added to Layer 3 without significant additional complexity. Lead should decide: implement in Phase 1 or defer to Phase 2.
atlas_city_names schema: Tyre's ARCH-3 schema needs population and economic_role columns (my flag above). If locked as-is, the name fulfillment pipeline cannot operate correctly.
For implementation tickets
WorldTier enum bug: Peripheral | Connected | Core → Epicenter | Regional | Backwater | Passage | Waypoint. First ticket, 0.5 dev-days. Blocking all downstream Layer 3-4 work.
atlas_city_names + atlas_feature_names creation: New tables in systems.db, populated by import_economics.py from wiki content. Blocked on wiki content having settlement lists (Miri's domain).
layout_irregularity field on DistrictSkeleton: Add layout_irregularity: f32 derived from founding_age interaction rules. Layer 3 architecture change.
Paula — Round 3. Written 2026-05-01.