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>
30 KiB
title, description, type, status, workshop, agent, round, created
| title | description | type | status | workshop | agent | round | created |
|---|---|---|---|---|---|---|---|
| Tyre — Round 3: Phase 3 Technical Architecture | 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 | workshop | active | generation-cascade | tyre | 3 | 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.
# 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:
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).
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:
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.
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:
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 quartersDockBay— ship berths, cargo handlingCommerceHub— trade, market, servicesIndustrialSection— manufacturing, processingAdminCore— command, administrationSecuritySection— law enforcement, detention
Connection topology: a graph of module adjacency. Each edge has a passage type (Pressurized | Airlock | EVA | Service).
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:
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
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:
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 correctfootprint_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-runwith the new regional sampling — compare output against a committed fixture (similar to howcheck-systems-db-stampworks). 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_directionsis 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:
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:
-
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.
-
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.
-
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.
-
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.
-
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
metatable - Includes itself in the
GENERATOR_SOURCESdict intooling/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) CityGenerationContextRust 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.