docs(governance): file D-242 corp-HQ settlement model + T-1074/1075/1076

Reviewing the T-1038 road-graph hub model surfaced that corp HQs are
cross-referenced into atlas_city_names as reserved rows — one per corp —
producing duplicate co-named "cities" (e.g. 10 'Groombridge' rows on
GJ380c) that the spatial layers place as separate settlements.

D-242 resolves the data model: corp HQs are not cities. The HQ relationship
lives with the corp; placement is a baked, specialization-keyed preference
(CityTenant vs Standalone, since corp_type is degenerate); population and
settlement_class are install-baked with only position seed-derived. This
supersedes the D-223 corp-HQ-cross-ref and deferred-pop/class notes.

Companion tickets under T-750: T-1074 (corp-HQ model / remove the cross-ref /
specialization map / standalone HQ settlements), T-1075 (bake per-settlement
population + settlement_class), T-1076 (road-graph hub refinement — scaled-cap
hubs, co-location collapse, hybrid minor attach, deferred RailHeadFacing pass;
blocked on T-1074 + T-1075). T-1038 annotated with shipped-vs-deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:13:10 +02:00
co-authored by Claude Opus 4.8
parent 1044939748
commit 301be77b49
6 changed files with 168 additions and 1 deletions
+2
View File
@@ -488,3 +488,5 @@ INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updat
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRYMFY5VFPXGANNRSXW', '06FB0TNSRX2TD5M8RQDJRNXA24', '2026-06-07 10:34:18', '2026-06-07 10:34:18', NULL, 'c50cf57d1ed4921ebffb718e02514ce5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRXFCP7EJV7RZK0TBF4', '06FB0TNSRXTPRZ40AC8HJGSQZ0', '2026-06-07 10:34:18', '2026-06-07 10:34:18', NULL, 'db5d2b25de9294320d1917f6444a73c9', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNSRYMFY5VFPXGANNRSXW', '06FB0TNSRW8ADSD8ZK32K7SWSC', '2026-06-07 16:35:38', '2026-06-07 16:35:38', NULL, 'adecdfdf040b687fd266c8bbf5a98616', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG73NW7SNCA5Y8JN905QMR', '06FCDG73V9R6N024QSFDEJ4R68', '2026-06-14 15:29:50', '2026-06-14 15:29:50', NULL, '389b010a55f227808e2b2e451acf8d36', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG73S4ET8KKTK62Z5KFEQM', '06FCDG73V9R6N024QSFDEJ4R68', '2026-06-14 15:29:50', '2026-06-14 15:29:50', NULL, '7e362812933c59ffe9f76ab069a83dc1', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
+51
View File
@@ -1864,3 +1864,54 @@ tooling/economy-db/import_economics.py (2,617 lines, 31 top-level defs) owns sch
Fix: extract one shared tooling/generator_sources.py imported by the script + stamp checker and read by /pr-push; then split importers/validators into modules under tooling/economy-db/; finish type hints during the split. Also fold in S-25''s connector dedup: move CONFIG_PATH/load_config/get_base_url into tooling/db/common.py (currently copy-pasted across audio/trellis/image connectors).
Closed 2026-06-12, commits 4f73624ef + 3c3fd0d0d (DB stamp regen). import_economics.py 2,620309 (thin orchestrator, exact CLI/transaction/exit-code contract); 13 modules under tooling/economy-db/economy_import/; tooling/generator_sources.py is the single registry (self-stamping, fail-closed module globbing) consumed by the importer, check-systems-db-stamp, and the pr-process watch list via --list. Connector config helpers centralized in tooling/db/common.py. Byte-identical proof: empty full-import dump diff (107,843 lines / 37 tables), dry-run parity, brands TOML sha unchanged. Gate green incl. test-tooling + stamp check on the regenerated DB.', NULL, '2026-06-12 18:15:04', '2026-06-12 18:15:04', '2026-06-12 18:15:04', NULL, '183b4254f288bf30b1feb6612ab712f5', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ02F02JDDBSFMGPX0XASR', 'status', 'backlog', 'in_progress', NULL, '2026-06-13 20:44:09', '2026-06-13 20:44:09', '2026-06-13 20:44:09', NULL, '1c65dd3a9d04be737f796df314c20959', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ02F02JDDBSFMGPX0XASR', 'status', 'in_progress', 'in_progress', NULL, '2026-06-13 20:44:30', '2026-06-13 20:44:30', '2026-06-13 20:44:30', NULL, '5c619f3ffe6b19f2112f1e2af21de542', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ02F02JDDBSFMGPX0XASR', 'description', 'Workshopped-but-unticketed cascade work captured 2026-06-12 (cascade-refocus grounding pass). Full grounded scope follows.
Generate the inter-settlement road/rail graph at Layer 2 the workshopped-but-never-ticketed link between settlement placement (T-955/T-956, done) and everything that consumes transport topology. No road code exists today (zero RoadGraph/road_segment hits in server/src; TerritorialStatus shipped without its road-coverage inputs).
Scope (algorithm fully decided, docs/workshops/planet-down-cascade/tyre-round1.md:274-299):
1. MST force-connect all CityPlacements (Euclidean), then secondary direct links for city pairs within 30-60% of body scale whose MST path exceeds 1.5x Euclidean.
2. Route each edge with A* over the L1 terrain-cost field (terrain_modification_cost, D-210, server/src/atlas/subbiome.rs; cost spec: water impassable, roughness>0.7 = 10.0, river corridor = 0.5, flat = 1.0 rivers as preferred valley routes). Budget ~30ms/body (round-1-notes.md:348).
3. Assign MaintenanceAuthority per edge from endpoint relationships (5-value enum Administrative/Corporate/Communal/Trade/Abandoned generation-cascade/workshop-outcomes.md:177, resolution :257; player-readable in road condition per ozzie-round1.md:86-92).
4. Place waypoint sub-settlement nodes at midpoints of edges > ~12 regional cells (tyre-round1.md:267-272).
5. Named-route identity assignment: post-generation join of generated edges to systems.db named highway routes via optional named_route_id FK (paula-round1.md:107,:140; round-1-notes.md:322 L2-Q3 resolution under Amendment 3).
6. RailHeadFacing second pass: settlements at high-connectivity road/rail junctions get FoundingOrientation::RailHeadFacing, overriding AdminFacing-from-Synthetic (paula-round3.md:218-236) wire into attractor_matching.rs.
Storage: RoadGraph{nodes,edges} on BodyWorldState (round-1-notes.md:257-267), session-cached, never serialized runtime Rust, not systems.db (the atlas_road_nodes/atlas_road_edges schema shape carries forward per tyre-round1.md:42 but lives in session memory).
Downstream consumers (do NOT scope in, but design the output for them): road_entry_directions per city (see road-entry-octants ticket; generation-cascade/round-3-notes.md:104), TerritorialStatus quantitative road-coverage thresholds (round-1-notes.md:329-338), per-economic_role hinterland fill (round-1-notes.md:324-327, BRIEF.md:279), prosperity/atlas overlays.
Cascade-first fit: this is direction step (2) the econ layer drawn onto the world between settlements. Parent: T-750. Suggested deps: blocks the road-entry-octants ticket; references D-205 (provinces), D-210, D-211, D-212, D-213.', 'Workshopped-but-unticketed cascade work captured 2026-06-12 (cascade-refocus grounding pass). Full grounded scope follows.
Generate the inter-settlement road/rail graph at Layer 2 the workshopped-but-never-ticketed link between settlement placement (T-955/T-956, done) and everything that consumes transport topology. No road code exists today (zero RoadGraph/road_segment hits in server/src; TerritorialStatus shipped without its road-coverage inputs).
Scope (algorithm fully decided, docs/workshops/planet-down-cascade/tyre-round1.md:274-299):
1. MST force-connect all CityPlacements (Euclidean), then secondary direct links for city pairs within 30-60% of body scale whose MST path exceeds 1.5x Euclidean.
2. Route each edge with A* over the L1 terrain-cost field (terrain_modification_cost, D-210, server/src/atlas/subbiome.rs; cost spec: water impassable, roughness>0.7 = 10.0, river corridor = 0.5, flat = 1.0 rivers as preferred valley routes). Budget ~30ms/body (round-1-notes.md:348).
3. Assign MaintenanceAuthority per edge from endpoint relationships (5-value enum Administrative/Corporate/Communal/Trade/Abandoned generation-cascade/workshop-outcomes.md:177, resolution :257; player-readable in road condition per ozzie-round1.md:86-92).
4. Place waypoint sub-settlement nodes at midpoints of edges > ~12 regional cells (tyre-round1.md:267-272).
5. Named-route identity assignment: post-generation join of generated edges to systems.db named highway routes via optional named_route_id FK (paula-round1.md:107,:140; round-1-notes.md:322 L2-Q3 resolution under Amendment 3).
6. RailHeadFacing second pass: settlements at high-connectivity road/rail junctions get FoundingOrientation::RailHeadFacing, overriding AdminFacing-from-Synthetic (paula-round3.md:218-236) wire into attractor_matching.rs.
Storage: RoadGraph{nodes,edges} on BodyWorldState (round-1-notes.md:257-267), session-cached, never serialized runtime Rust, not systems.db (the atlas_road_nodes/atlas_road_edges schema shape carries forward per tyre-round1.md:42 but lives in session memory).
Downstream consumers (do NOT scope in, but design the output for them): road_entry_directions per city (see road-entry-octants ticket; generation-cascade/round-3-notes.md:104), TerritorialStatus quantitative road-coverage thresholds (round-1-notes.md:329-338), per-economic_role hinterland fill (round-1-notes.md:324-327, BRIEF.md:279), prosperity/atlas overlays.
Cascade-first fit: this is direction step (2) the econ layer drawn onto the world between settlements. Parent: T-750. Suggested deps: blocks the road-entry-octants ticket; references D-205 (provinces), D-210, D-211, D-212, D-213.
---
Implementation (2026-06-14):
SHIPPED deterministic road-graph CORE in server/src/atlas/road_graph.rs (CascadeLayer::RoadGraph, stored on BodyWorldState.road_graph, never serialized):
1. MST force-connect (strict Kruskal over dist^2) + secondary links (30-60% body-scale band, >1.5x MST detour).
2. A* over the L1 terrain-cost field (ocean/lake impassable, river 1/2 cost, slope>=35deg 10x, else D-210 terrain_modification_cost) on a coarse 64-wide routing grid; equirectangular column wrap.
3. MaintenanceAuthority per edge (new generator.rs enum: Administrative/Corporate/Communal/Trade/Abandoned) from endpoint political archetypes + haul length.
4. Waypoint nodes at midpoints of edges > 12 routing cells.
5. Named-route identity join (named_route_id, designed-for; pool empty post-D-223 -> no-op).
+ junction detection (RoadGraph::high_connectivity_junctions, degree>=3).
Wired into the cascade (gen_queue requests CascadeLayer::RoadGraph) + BodyWorldState. 10 road_graph unit tests + a cascade e2e test; full lib suite green.
DEFERRED (-> T-1076, gated on the D-242 settlement model T-1074/T-1075):
- step 6 RailHeadFacing second pass the adopted FoundingOrientation enum (D-213) has no RailHeadFacing; needs a D-213 amendment + Layer-4 skeleton_gen handling. Junction data is already exposed for it.
- hub-selection refinement (scaled-cap hubs, co-location collapse, hybrid minor-settlement attach) needs baked population/specialization (D-242).
Decision D-242 filed (corp-HQ settlement model) resolves why corp HQs were polluting the city pool as duplicate settlements.', NULL, '2026-06-14 15:30:58', '2026-06-14 15:30:58', '2026-06-14 15:30:58', NULL, 'adca3fc7b58c20d9805752a9082817a4', 2) ON CONFLICT(hash) DO NOTHING;
+3
View File
@@ -81,3 +81,6 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPTXSEC1F0RKJVH7JVPS8NW', 'T-1071', '2026-06-12 10:40:59', '2026-06-12 10:40:59', NULL, 'b3f82a325e1724e3d218fe54ba344f64', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ8WGMA3552756BQAAFPMER', 'T-1072', '2026-06-12 11:41:59', '2026-06-12 11:41:59', NULL, 'fefd6fa8565a07b57ff8bbd5cadfeb7e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBRCMEKT72S0Q6NE68JH7EXC', 'T-1073', '2026-06-12 14:18:10', '2026-06-12 14:18:10', NULL, 'e2373f7a83c096537e649e733ccde52f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG73NW7SNCA5Y8JN905QMR', 'T-1074', '2026-06-14 15:29:50', '2026-06-14 15:29:50', NULL, '2bf2aa1e4d416127b28f923b273bfd67', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG73S4ET8KKTK62Z5KFEQM', 'T-1075', '2026-06-14 15:29:50', '2026-06-14 15:29:50', NULL, '5227303769a48815b53724fb47d42b39', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG73V9R6N024QSFDEJ4R68', 'T-1076', '2026-06-14 15:29:50', '2026-06-14 15:29:50', NULL, '8df24639ed7dc3c6a29bc2019da02f74', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
+92
View File
@@ -2127,3 +2127,95 @@ tooling/economy-db/import_economics.py (2,617 lines, 31 top-level defs) owns sch
Fix: extract one shared tooling/generator_sources.py imported by the script + stamp checker and read by /pr-push; then split importers/validators into modules under tooling/economy-db/; finish type hints during the split. Also fold in S-25''s connector dedup: move CONFIG_PATH/load_config/get_base_url into tooling/db/common.py (currently copy-pasted across audio/trellis/image connectors).
Closed 2026-06-12, commits 4f73624ef + 3c3fd0d0d (DB stamp regen). import_economics.py 2,620309 (thin orchestrator, exact CLI/transaction/exit-code contract); 13 modules under tooling/economy-db/economy_import/; tooling/generator_sources.py is the single registry (self-stamping, fail-closed module globbing) consumed by the importer, check-systems-db-stamp, and the pr-process watch list via --list. Connector config helpers centralized in tooling/db/common.py. Byte-identical proof: empty full-import dump diff (107,843 lines / 37 tables), dry-run parity, brands TOML sha unchanged. Gate green incl. test-tooling + stamp check on the regenerated DB.', 'done', 'medium', NULL, NULL, NULL, '2026-06-12 10:40:59', '2026-06-12 18:15:04', NULL, 'e41a21e215fc14cfd3471d1a7d01a960', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ02F02JDDBSFMGPX0XASR', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Layer 2 — inter-settlement road/rail graph (MST + A* over terrain cost, MaintenanceAuthority, named-route identity)', 'Workshopped-but-unticketed cascade work captured 2026-06-12 (cascade-refocus grounding pass). Full grounded scope follows.
Generate the inter-settlement road/rail graph at Layer 2 the workshopped-but-never-ticketed link between settlement placement (T-955/T-956, done) and everything that consumes transport topology. No road code exists today (zero RoadGraph/road_segment hits in server/src; TerritorialStatus shipped without its road-coverage inputs).
Scope (algorithm fully decided, docs/workshops/planet-down-cascade/tyre-round1.md:274-299):
1. MST force-connect all CityPlacements (Euclidean), then secondary direct links for city pairs within 30-60% of body scale whose MST path exceeds 1.5x Euclidean.
2. Route each edge with A* over the L1 terrain-cost field (terrain_modification_cost, D-210, server/src/atlas/subbiome.rs; cost spec: water impassable, roughness>0.7 = 10.0, river corridor = 0.5, flat = 1.0 rivers as preferred valley routes). Budget ~30ms/body (round-1-notes.md:348).
3. Assign MaintenanceAuthority per edge from endpoint relationships (5-value enum Administrative/Corporate/Communal/Trade/Abandoned generation-cascade/workshop-outcomes.md:177, resolution :257; player-readable in road condition per ozzie-round1.md:86-92).
4. Place waypoint sub-settlement nodes at midpoints of edges > ~12 regional cells (tyre-round1.md:267-272).
5. Named-route identity assignment: post-generation join of generated edges to systems.db named highway routes via optional named_route_id FK (paula-round1.md:107,:140; round-1-notes.md:322 L2-Q3 resolution under Amendment 3).
6. RailHeadFacing second pass: settlements at high-connectivity road/rail junctions get FoundingOrientation::RailHeadFacing, overriding AdminFacing-from-Synthetic (paula-round3.md:218-236) wire into attractor_matching.rs.
Storage: RoadGraph{nodes,edges} on BodyWorldState (round-1-notes.md:257-267), session-cached, never serialized runtime Rust, not systems.db (the atlas_road_nodes/atlas_road_edges schema shape carries forward per tyre-round1.md:42 but lives in session memory).
Downstream consumers (do NOT scope in, but design the output for them): road_entry_directions per city (see road-entry-octants ticket; generation-cascade/round-3-notes.md:104), TerritorialStatus quantitative road-coverage thresholds (round-1-notes.md:329-338), per-economic_role hinterland fill (round-1-notes.md:324-327, BRIEF.md:279), prosperity/atlas overlays.
Cascade-first fit: this is direction step (2) the econ layer drawn onto the world between settlements. Parent: T-750. Suggested deps: blocks the road-entry-octants ticket; references D-205 (provinces), D-210, D-211, D-212, D-213.', 'in_progress', 'high', NULL, 'server', 'D-211', '2026-06-12 10:23:49', '2026-06-13 20:44:09', NULL, 'e178b187eaa605cf4d6edccf77304487', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ02F02JDDBSFMGPX0XASR', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Layer 2 — inter-settlement road/rail graph (MST + A* over terrain cost, MaintenanceAuthority, named-route identity)', 'Workshopped-but-unticketed cascade work captured 2026-06-12 (cascade-refocus grounding pass). Full grounded scope follows.
Generate the inter-settlement road/rail graph at Layer 2 the workshopped-but-never-ticketed link between settlement placement (T-955/T-956, done) and everything that consumes transport topology. No road code exists today (zero RoadGraph/road_segment hits in server/src; TerritorialStatus shipped without its road-coverage inputs).
Scope (algorithm fully decided, docs/workshops/planet-down-cascade/tyre-round1.md:274-299):
1. MST force-connect all CityPlacements (Euclidean), then secondary direct links for city pairs within 30-60% of body scale whose MST path exceeds 1.5x Euclidean.
2. Route each edge with A* over the L1 terrain-cost field (terrain_modification_cost, D-210, server/src/atlas/subbiome.rs; cost spec: water impassable, roughness>0.7 = 10.0, river corridor = 0.5, flat = 1.0 rivers as preferred valley routes). Budget ~30ms/body (round-1-notes.md:348).
3. Assign MaintenanceAuthority per edge from endpoint relationships (5-value enum Administrative/Corporate/Communal/Trade/Abandoned generation-cascade/workshop-outcomes.md:177, resolution :257; player-readable in road condition per ozzie-round1.md:86-92).
4. Place waypoint sub-settlement nodes at midpoints of edges > ~12 regional cells (tyre-round1.md:267-272).
5. Named-route identity assignment: post-generation join of generated edges to systems.db named highway routes via optional named_route_id FK (paula-round1.md:107,:140; round-1-notes.md:322 L2-Q3 resolution under Amendment 3).
6. RailHeadFacing second pass: settlements at high-connectivity road/rail junctions get FoundingOrientation::RailHeadFacing, overriding AdminFacing-from-Synthetic (paula-round3.md:218-236) wire into attractor_matching.rs.
Storage: RoadGraph{nodes,edges} on BodyWorldState (round-1-notes.md:257-267), session-cached, never serialized runtime Rust, not systems.db (the atlas_road_nodes/atlas_road_edges schema shape carries forward per tyre-round1.md:42 but lives in session memory).
Downstream consumers (do NOT scope in, but design the output for them): road_entry_directions per city (see road-entry-octants ticket; generation-cascade/round-3-notes.md:104), TerritorialStatus quantitative road-coverage thresholds (round-1-notes.md:329-338), per-economic_role hinterland fill (round-1-notes.md:324-327, BRIEF.md:279), prosperity/atlas overlays.
Cascade-first fit: this is direction step (2) the econ layer drawn onto the world between settlements. Parent: T-750. Suggested deps: blocks the road-entry-octants ticket; references D-205 (provinces), D-210, D-211, D-212, D-213.', 'in_progress', 'high', NULL, 'server', 'D-211', '2026-06-12 10:23:49', '2026-06-13 20:44:30', NULL, '591936c6e539ddf25905176ce059b676', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG73NW7SNCA5Y8JN905QMR', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Corp HQ settlement model — remove HQs from the city pool; specialization-keyed standalone-vs-tenant; install-baked', 'Per D-242. Corp HQs are not cities and must leave atlas_city_names.
1. Remove the D-207/D-223 reserved=1 corp-HQ cross-reference (populate_atlas_city_names_corps, tooling/economy-db/economy_import/atlas.py) it inserts one atlas_city_names row per corp HQ, creating duplicate co-named cities (e.g. 10 Groombridge rows on GJ380c).
2. Author a source-canonical specialization -> HQ-placement map ({CityTenant, Standalone}). corp_type is degenerate (corporation/combine only); key on corporations.specialization (and/or supply_chain_role). Install-baked.
3. CityTenant HQs: a corp->city link, no new settlement (many corps may tenant one city).
4. Standalone HQs (extraction/heavy-industry/shipyards): emit their own settlement (body + position) into the cascade settlement list. Requires resolving headquarters_body (today 0/165 set; only headquarters_system + a most-populated-body heuristic).
5. Cascade settlement input becomes: city pool + standalone-HQ settlements, merged.
Amends D-207, D-223. Blocks the road-graph hub refinement.', 'backlog', 'high', NULL, 'server', 'D-242', '2026-06-14 15:29:50', '2026-06-14 15:29:50', NULL, '9176515b9f67459f2dcee6c58de1e0fa', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG73S4ET8KKTK62Z5KFEQM', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Bake per-settlement population + settlement_class into the install dataset (only position stays seed-derived)', 'Per D-242. population + settlement_class are install-baked, not seed-derived.
Today: body-level population is baked (273/3240 bodies > 0), but per-city population is 0 and settlement_class NULL for all 463 atlas_city_names rows. D-223 deferred these ''to placement (T-955)'', but match_cities only READS them for tiering nothing assigns them, so the pop-from-seed step was never built and all cities fall to synthetic placement.
Bake the per-settlement population spread + settlement_class into the source data (importer/source TOMLs). Only POSITION stays seed-derived (D-211). Drop the never-built seed-population idea.
Supersedes the D-223 deferred-pop/class note. Enables significance-ranked hub selection.', 'backlog', 'high', NULL, 'server', 'D-242', '2026-06-14 15:29:50', '2026-06-14 15:29:50', NULL, 'a3e992272c50209fb1dfffbcfe6ea28c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG73V9R6N024QSFDEJ4R68', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Road-graph hub refinement — scaled-cap hubs + co-location collapse + hybrid minor attach + RailHeadFacing junction pass', 'Follow-on to T-1038 (road-graph core shipped). Hub-selection refinement, gated on the resolved settlement model (D-242).
Build on server/src/atlas/road_graph.rs:
1. Hub selection: ''real cities, scaled cap'' wiki cities anchor the MST; cap scales with body size; standalone HQs/minor settlements are non-hub.
2. Co-location collapse: group same-named settlements (defensive; the D-242 corp-HQ fix removes most at source).
3. Hybrid minor-settlement attach: snap to nearest existing road edge if close, else spur to nearest hub (add edge-split-at-attach).
4. RailHeadFacing junction pass (T-1038 step 6, deferred): junction settlements (RoadGraph::high_connectivity_junctions already computed) get FoundingOrientation::RailHeadFacing. REQUIRES a D-213 amendment (adopted enum has no RailHeadFacing) + Layer-4 skeleton_gen handling.
Needs population ranking blocked on the corp-HQ-model + population-bake tickets.', 'backlog', 'medium', NULL, 'server', 'D-242', '2026-06-14 15:29:50', '2026-06-14 15:29:50', NULL, 'a9b2ff68ee5d9208cffb0a1ebe63bbe2', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ02F02JDDBSFMGPX0XASR', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Layer 2 — inter-settlement road/rail graph (MST + A* over terrain cost, MaintenanceAuthority, named-route identity)', 'Workshopped-but-unticketed cascade work captured 2026-06-12 (cascade-refocus grounding pass). Full grounded scope follows.
Generate the inter-settlement road/rail graph at Layer 2 the workshopped-but-never-ticketed link between settlement placement (T-955/T-956, done) and everything that consumes transport topology. No road code exists today (zero RoadGraph/road_segment hits in server/src; TerritorialStatus shipped without its road-coverage inputs).
Scope (algorithm fully decided, docs/workshops/planet-down-cascade/tyre-round1.md:274-299):
1. MST force-connect all CityPlacements (Euclidean), then secondary direct links for city pairs within 30-60% of body scale whose MST path exceeds 1.5x Euclidean.
2. Route each edge with A* over the L1 terrain-cost field (terrain_modification_cost, D-210, server/src/atlas/subbiome.rs; cost spec: water impassable, roughness>0.7 = 10.0, river corridor = 0.5, flat = 1.0 rivers as preferred valley routes). Budget ~30ms/body (round-1-notes.md:348).
3. Assign MaintenanceAuthority per edge from endpoint relationships (5-value enum Administrative/Corporate/Communal/Trade/Abandoned generation-cascade/workshop-outcomes.md:177, resolution :257; player-readable in road condition per ozzie-round1.md:86-92).
4. Place waypoint sub-settlement nodes at midpoints of edges > ~12 regional cells (tyre-round1.md:267-272).
5. Named-route identity assignment: post-generation join of generated edges to systems.db named highway routes via optional named_route_id FK (paula-round1.md:107,:140; round-1-notes.md:322 L2-Q3 resolution under Amendment 3).
6. RailHeadFacing second pass: settlements at high-connectivity road/rail junctions get FoundingOrientation::RailHeadFacing, overriding AdminFacing-from-Synthetic (paula-round3.md:218-236) wire into attractor_matching.rs.
Storage: RoadGraph{nodes,edges} on BodyWorldState (round-1-notes.md:257-267), session-cached, never serialized runtime Rust, not systems.db (the atlas_road_nodes/atlas_road_edges schema shape carries forward per tyre-round1.md:42 but lives in session memory).
Downstream consumers (do NOT scope in, but design the output for them): road_entry_directions per city (see road-entry-octants ticket; generation-cascade/round-3-notes.md:104), TerritorialStatus quantitative road-coverage thresholds (round-1-notes.md:329-338), per-economic_role hinterland fill (round-1-notes.md:324-327, BRIEF.md:279), prosperity/atlas overlays.
Cascade-first fit: this is direction step (2) the econ layer drawn onto the world between settlements. Parent: T-750. Suggested deps: blocks the road-entry-octants ticket; references D-205 (provinces), D-210, D-211, D-212, D-213.
---
Implementation (2026-06-14):
SHIPPED deterministic road-graph CORE in server/src/atlas/road_graph.rs (CascadeLayer::RoadGraph, stored on BodyWorldState.road_graph, never serialized):
1. MST force-connect (strict Kruskal over dist^2) + secondary links (30-60% body-scale band, >1.5x MST detour).
2. A* over the L1 terrain-cost field (ocean/lake impassable, river 1/2 cost, slope>=35deg 10x, else D-210 terrain_modification_cost) on a coarse 64-wide routing grid; equirectangular column wrap.
3. MaintenanceAuthority per edge (new generator.rs enum: Administrative/Corporate/Communal/Trade/Abandoned) from endpoint political archetypes + haul length.
4. Waypoint nodes at midpoints of edges > 12 routing cells.
5. Named-route identity join (named_route_id, designed-for; pool empty post-D-223 -> no-op).
+ junction detection (RoadGraph::high_connectivity_junctions, degree>=3).
Wired into the cascade (gen_queue requests CascadeLayer::RoadGraph) + BodyWorldState. 10 road_graph unit tests + a cascade e2e test; full lib suite green.
DEFERRED (-> T-1076, gated on the D-242 settlement model T-1074/T-1075):
- step 6 RailHeadFacing second pass the adopted FoundingOrientation enum (D-213) has no RailHeadFacing; needs a D-213 amendment + Layer-4 skeleton_gen handling. Junction data is already exposed for it.
- hub-selection refinement (scaled-cap hubs, co-location collapse, hybrid minor-settlement attach) needs baked population/specialization (D-242).
Decision D-242 filed (corp-HQ settlement model) resolves why corp HQs were polluting the city pool as duplicate settlements.', 'in_progress', 'high', NULL, 'server', 'D-211', '2026-06-12 10:23:49', '2026-06-14 15:30:58', NULL, '55c0e158089122497cf34e9574595d50', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+1
View File
@@ -307,6 +307,7 @@ line in place — keep the Q-record for the audit trail rather than deleting it.
- [D-239: Tile derivation contract — coarse→fine refinement chain (resolves Q-101)](decisions/architecture.md#d-239-tile-derivation-contract--coarsefine-refinement-chain-resolves-q-101) — _architecture_
- [D-240: Registry orbit/star data is non-canonical — climate derives from a `planet_class` temperature envelope](decisions/architecture.md#d-240-registry-orbitstar-data-is-non-canonical--climate-derives-from-a-planet-class-temperature-envelope) — _architecture_
- [D-241: Spike binary artifacts are local-only — gitignored, never tracked](decisions/process.md#d-241-spike-binary-artifacts-are-local-only--gitignored-never-tracked) — _process_
- [D-242: Corp HQ settlement model — HQs are not cities; separate from the name pool, specialization-keyed, install-baked](decisions/architecture.md#d-242-corp-hq-settlement-model--hqs-are-not-cities-separate-from-the-name-pool-specialization-keyed-install-baked) — _architecture_
## Open questions
+19 -1
View File
@@ -1787,4 +1787,22 @@ Technical foundation decisions that constrain implementation: engine, client-ser
---
*98 decisions (D-001 through D-240, excluding gaps). Last updated: 2026-06-08 (climate-canonicity correction — D-240 declares registry orbit/star data non-canonical; temperature flips from sun-driven [D-239 §2] to a `planet_class` envelope, superseding the Stefan-Boltzmann path that produced +356 °C "temperate" worlds).*
### D-242: Corp HQ settlement model — HQs are not cities; separate from the name pool, specialization-keyed, install-baked
- **Date:** 2026-06-14
- **Decision:** A corporate **headquarters is not a city** and does not belong in the `atlas_city_names` flavored name pool.
- **Remove the corp-HQ cross-reference into the city pool.** The [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features)/[D-223](#d-223) `reserved=1` step (`populate_atlas_city_names_corps`, `tooling/economy-db/economy_import/atlas.py`) inserts **one `atlas_city_names` row per corp HQ** whose name isn't already pooled. With no `UNIQUE(body_id, name)` it produces **duplicate co-named "cities"** — e.g. **10 `Groombridge` rows on GJ380c**, one per corp — which every downstream spatial layer (placement, roads, hinterland) then renders as separate settlements scattered across the map. This insert path is removed; the HQ relationship lives with the corp (`corporations.headquarters_system`, plus a resolved `headquarters_body`), not in the city pool. This **supersedes [D-223](#d-223)'s** "the `reserved=1` corp-HQ cross-reference stays" note.
- **HQ placement is a baked, specialization-keyed preference.** Each corp's HQ is `CityTenant` or `Standalone`, chosen by a **source-canonical `specialization → HQ-placement` map** (`corp_type` is degenerate — only `corporation`/`combine`, 158/7 — so the key is `corporations.specialization`, optionally `supply_chain_role`):
- **CityTenant** (finance, media, retail, services …): the HQ is a **tenant of an existing city** — a corp→city link, **no new settlement**. Many corps may tenant one city (supplemental, non-competing HQs realistically cluster — a bank and a newspaper share a downtown).
- **Standalone** (extraction/mining, heavy industry, shipyards …): the HQ **is its own settlement**, emitted into the cascade settlement list with its own body + position, so the road graph and downstream layers treat it as a place.
- The preference depends only on corp specialization — **not on seed or install** — so it is authored once and baked into `systems.db` at import (a difference of corp *type*, not of run).
- **`population` and `settlement_class` are install-baked, not seed-derived.** Body-level `population` is already baked (273/3 240 bodies > 0); the **per-settlement spread is carried down into the city/settlement source data**, so every settlement starts with a fixed `population` + `settlement_class`. **Only position remains seed-derived** (the [D-211](#d-211) placement pipeline). This **supersedes [D-223](#d-223)'s** "`population`/`settlement_class` deferred to placement (T-955)" and **retires the never-built "assign population from the seed" step**`match_cities` only ever *read* those fields (all `0`/NULL) and nothing assigned them, so faking population in the seed layer buys nothing over baking it.
- **The cascade settlement input** = the city name pool (cities, baked pop/class) **+** standalone-HQ settlements, merged. The Layer-2 road graph ([D-211](#d-211), T-1038) routes over that merged list unchanged; "hubs" are the significant cities, standalone HQs are typically minor nodes.
- **Rationale:** Modelling HQs as city rows conflated two different things and made corp-dense worlds nonsensical — ten "Groombridge" dots, ten trunk roads, where there is one city hosting ten tenants. HQs *are* sometimes their own settlement (a mine or shipyard sits alone) and *sometimes* a tenant (a bank wants a downtown); that split is intrinsic to the corp's specialization and never varies by run, so it is authored, not derived. Baking `population`/`settlement_class` (rather than deriving them from the seed) keeps generation deterministic with **only position variable**, matches the already-baked body population, and deletes an unbuilt seed step instead of building it. The names pool ([D-223](#d-223)) returns to being **cities only**.
- **Raised by:** Jeroen, 2026-06-14 — reviewing the T-1038 road-graph hub model; the corp-HQ-as-city rows surfaced as duplicate map settlements.
- **Implementation:** corp-HQ model + specialization map + standalone-HQ settlements, remove the cross-ref insert, resolve `headquarters_body` (**T-1074**); per-settlement population/settlement_class baking (**T-1075**); and the gated road-graph hub refinement — scaled-cap hubs + co-location collapse + hybrid minor-settlement attach + the deferred `RailHeadFacing` junction pass (which itself needs a [D-213](#d-213) amendment) — (**T-1076**, blocked by T-1074 + T-1075). All under Phase 4 (epic T-750). The T-1038 road-graph **core** (MST + A\* + MaintenanceAuthority + waypoints + junction detection) ships independently of this record — it routes over whatever settlement list it is handed.
- **Cross-reference:** [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features) (corp-HQ cross-ref — the insert path is removed here), [D-223](#d-223) (names-only pool — the corp-HQ-stays + pop/class-deferred notes are superseded here), [D-211](#d-211) (placement — consumes the merged list; only position is seed-derived), [D-213](#d-213) (`FoundingOrientation` — a `RailHeadFacing` variant is the gated follow-on), [D-237](#d-237) (corp `specialization` vocabulary — the HQ-placement key), [D-010](#d-010) (determinism — only position is seed-derived).
- **Dissent:** None
---
*99 decisions (D-001 through D-242, excluding gaps). Last updated: 2026-06-14 (corp-HQ settlement model — D-242: HQs leave the city name pool, HQ placement is a specialization-keyed CityTenant/Standalone preference, population/settlement_class become install-baked with only position seed-derived; supersedes the D-223 corp-HQ-cross-ref and deferred-pop/class notes).*