Files
settled-reach/docs/workshops/planet-down-cascade/paula-round3.md
T
jpmschweitzerandClaude Opus 4.6 b9fd75b840 docs(workshops): planet-down cascade workshop + misc stray files
Planet-down cascade workshop (3 rounds, 5 agents): layer-by-layer
generation from empty world through population overlay, city planning,
and street rendering. Includes consultant review by Troblum.

Also commits: pre-Sprint-35 DB backup, Claude Code team-mode tmux
test log (team-test.md).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-03 20:18:30 +02:00

42 KiB
Raw Blame History

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:

  1. Layer 1 produces a coarse river network at Province resolution (drainage basin edges)
  2. The planetary atlas UI renders this river network as thin geographic lines
  3. Province boundaries are the visible boundaries between drainage basins — rendered by the river network, not as political overlays
  4. 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.150.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.01.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: [3050%] 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 [2040%] 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.150.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 (H1H4, 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.150.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_modelayout_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 | CoreEpicenter | 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.