From c0de8ce68ca9aa79877647511716c2bd3ac8f285 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 16 Jul 2026 10:29:34 +0200 Subject: [PATCH] docs(meta): D-242 implementation status + derivation amendment; D-207/D-223 supersede notes (T-1074/T-1075) Co-Authored-By: Claude Fable 5 --- governance/decisions/architecture.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 56b0596b1..cd5d24649 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1030,6 +1030,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser ### D-207: Fully Generative Placement — markers.json Stripped to Topographic Features - **Date:** 2026-05-01 - **Amendment (2026-05-22, [D-223](#d-223)):** superseded in part. markers.json no longer retains topographic-feature *geometry* — it is reduced to **names only** (a flavored name pool). River/mountain positions are derived from the heightmap + drainage (D-208); settlement positions come from the economic sim + placement (D-211). The `reserved=1` pinning and the 6 hand-authored templates (Lendel, Edict, Vuurkloof, Røros, Cairnside, Estrade) are removed — their bodies remain as ordinary named bodies, only the authored machinery is gone. See D-223. +- **Amendment (2026-07-16, [D-242](#d-242)):** the `corp_id` column and the "corp HQ cross-reference" (T-909, `populate_atlas_city_names_corps`) this record's `Decision` section describes below are **removed**. That insert path (kept alive through D-223's amendment above) had no `UNIQUE(body_id, name)` and produced duplicate co-named "cities" — one `atlas_city_names` row per corp HQ (e.g. 10 `Groombridge` rows on GJ380c). The corp↔settlement relationship now lives with the corp (`corporations.headquarters_body`/`headquarters_city_id`), not as a row in the city pool. `atlas_city_names.corp_id` is left in the schema (additive-safe) but is permanently `NULL` going forward — see D-242. - **Decision:** The `atlas_city_names` table replaces the authored city positions in `markers.json`. Going forward, `markers.json` files contain only topographic features (rivers, oceans, mountain ranges — per D-191 §8 canonical format). City positions, road networks, and rail networks are NOT authored in `markers.json`; they are generated from the terrain data and stored in `atlas_city_names` and derived tables. ```sql CREATE TABLE atlas_city_names ( @@ -1467,9 +1468,9 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **Rationale:** Authored positions created a hand/procedural split that (per D-207's own rationale) was impossible to query, diff, or validate. Reducing authored content to a name pool removes that split entirely while preserving the flavor that makes places feel hand-made — names follow culture and region, geometry follows terrain and economics. The templates were leftover scaffolding from before fully-generative placement; removing them simplifies the pipeline without losing any canonical place. - **Implementation:** tracked under Phase 4 (epic T-750) — strip markers to names, remove template machinery + `reserved` pinning, update the atlas pipeline/schema, regen. Historical archives (sprints, workshops, audits, CHANGELOG, atlas proposals) are left untouched. -- **Implementation status (T-951, 2026-05-22): done.** All 2,398 `markers.json` reduced to a names-only pool (25,264 names; 349 city names across 271 bodies). The Python atlas **geometry generator** (`generate_atlas.py`) and the **LLM naming cluster** (`gemma_naming.py`, `naming_core.py`, `apply_name_fixes.py`, their tests/QA, the `fix_fewshot_bleed`/`prune_atlas_features` geometry tools, the redundant `import_city_names.py`, and the `run-atlas-naming.sh` runner) were retired — the procedural server cascade (Phase 4) supersedes them. The Gemma prompting methodology is preserved in [docs/gemma-naming-methodology.md](../docs/gemma-naming-methodology.md). Shared atlas-DB utilities moved to `tooling/planet-gen/atlas_common.py`; **`import_economics.py` is now the sole regen-db generator that owns the atlas index** — it loads the names pool into `atlas_city_names` and empties the 8 geometry tables (`atlas_cities/roads/railroads/pois/rivers/oceans/mountain_ranges/body_grids`), which the cascade fills. `population`/`kind`/`settlement_class` on `atlas_city_names` are **deferred to placement (T-955)** — `attractor_matching` reads them at 0/default until then; that empty state is the gap the cascade closes. The `reserved=1` corp-HQ cross-reference stays (corp HQ names are real places). A latent duplicate-accumulation bug in name population (no clear + no unique constraint) was fixed with a deterministic rebuild. **Sol (system `GJ 0`) is permanently exempt from the normal generators:** it uses real Earth/Mars/Luna geography via the offline `sol_import.py` (left in place for future scripted integration), so its bodies keep geometry-bearing `markers.json` as preserved positional config and are skipped by the names-pool importer — Sol names will come from its own integration, not the cascade. `make regen-db` green. +- **Implementation status (T-951, 2026-05-22): done.** All 2,398 `markers.json` reduced to a names-only pool (25,264 names; 349 city names across 271 bodies). The Python atlas **geometry generator** (`generate_atlas.py`) and the **LLM naming cluster** (`gemma_naming.py`, `naming_core.py`, `apply_name_fixes.py`, their tests/QA, the `fix_fewshot_bleed`/`prune_atlas_features` geometry tools, the redundant `import_city_names.py`, and the `run-atlas-naming.sh` runner) were retired — the procedural server cascade (Phase 4) supersedes them. The Gemma prompting methodology is preserved in [docs/gemma-naming-methodology.md](../docs/gemma-naming-methodology.md). Shared atlas-DB utilities moved to `tooling/planet-gen/atlas_common.py`; **`import_economics.py` is now the sole regen-db generator that owns the atlas index** — it loads the names pool into `atlas_city_names` and empties the 8 geometry tables (`atlas_cities/roads/railroads/pois/rivers/oceans/mountain_ranges/body_grids`), which the cascade fills. ~~`population`/`kind`/`settlement_class` on `atlas_city_names` are deferred to placement (T-955)~~ **SUPERSEDED (D-242, T-1075):** both are now install-baked at import time (a documented rank-size derivation from `bodies.population`; `settlement_class` defaults `PopulationBudget` with a small `NameLocked` override list) — see D-242. ~~The `reserved=1` corp-HQ cross-reference stays (corp HQ names are real places)~~ **SUPERSEDED (D-242, T-1074):** that insert path produced duplicate co-named "cities" (no `UNIQUE(body_id, name)`) and is removed; the corp↔settlement relationship now lives on `corporations`, not as a city-pool row — see D-242. A latent duplicate-accumulation bug in name population (no clear + no unique constraint) was fixed with a deterministic rebuild. **Sol (system `GJ 0`) is permanently exempt from the normal generators:** it uses real Earth/Mars/Luna geography via the offline `sol_import.py` (left in place for future scripted integration), so its bodies keep geometry-bearing `markers.json` as preserved positional config and are skipped by the names-pool importer — Sol names will come from its own integration, not the cascade. `make regen-db` green. - **Raised by:** Jeroen, 2026-05-22 — resolving the open question on merging preconfigured content into the deterministic cascade. -- **Cross-reference:** [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features) (superseded — names-only, no reserved pinning), [D-191](#d-191) §8 (markers format — names-only), [D-208](#d-208) (drainage → river courses), [D-211](#d-211) (settlement placement), [D-199](#d-199) (economic read set), [D-222](#d-222) (lore≠code names on generated geometry) +- **Cross-reference:** [D-207](#d-207-fully-generative-placement--markersjson-stripped-to-topographic-features) (superseded — names-only, no reserved pinning), [D-191](#d-191) §8 (markers format — names-only), [D-208](#d-208) (drainage → river courses), [D-211](#d-211) (settlement placement), [D-199](#d-199) (economic read set), [D-222](#d-222) (lore≠code names on generated geometry), [D-242](#d-242) (corp-HQ cross-reference removed + population/settlement_class baking — both supersede notes above) - **Dissent:** None --- @@ -1818,7 +1819,16 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **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). +- **Implementation status (T-1074/T-1075, 2026-07-16): done.** Both stories landed together, in order. + - **`corp_type` was already known degenerate** (158 `corporation` / 7 `combine`) — confirmed the key is `corporations.specialization`, not `corp_type`. That free-text `specialization` column is legacy prose (only 20/165 rows ever populated, by the orphaned `tooling/populate-corporations.sh` — **retired/deleted**, T-1074 point 6) and is left alone (`generate_corporations`'s fuzzy brand-relevance match still reads it). The HQ-placement key is a **new, separate column**: `corporations.corp_specialization`. + - **Vocabulary choice — reused AND extended, per this record's own instruction to prefer reuse.** `corp_specialization` reuses the [D-237](#d-237) `specialization_vocabulary` id-space directly rather than a parallel corp-only taxonomy — every value already carries `bulk_class_projected` (`NonPhysical` vs everything else), which *is* the CityTenant/Standalone signal this record calls for. Backfilling all 165 corps (155 with a wiki page; the categorization pass read every one) found the 27 system-authored values genuinely under-cover corp-shaped identities: ~10% of corps do **trade/logistics distribution**, **hospitality/tourism**, or **general professional/advisory** work — moving or selling what someone else made, not producing it — which none of the 27 values name. Three values were **added** to `specialization_vocabulary.toml` (all `NonPhysical` → `CityTenant`): `trade_distribution`, `hospitality_hub`, `professional_services`. The vocabulary is now 30 values, shared by two authored axes: `system_economy.economic_specialization` ([D-237](#d-237), system-scale) and `corporations.corp_specialization` (this record, corp-scale) — nothing stops a future system from adopting the 3 new values too. Per-corp authoring lives in wiki frontmatter (`wiki/corporations/*.md`, key `corp_specialization`) — the natural per-corp source, parsed by `_parse_corp_frontmatter`/`load_wiki_corps` (extended) — not a per-corp TOML stanza file (unlike D-237's per-*system* TOML, 165 corps would make one file unwieldy; one wiki page per corp already exists and is the authoritative source for everything else about it). + - **The placement map is authored explicitly, not computed from `bulk_class_projected` at import time** — `wiki/economics/corp_hq_placement.toml`, one `hq_placement` stanza per vocabulary value (all 30), defaulting to the `NonPhysical` rule but reviewable/overridable per value without a code change. Standalone stanzas also carry `standalone_economic_role`, mapping the 30-value specialization onto the pre-existing 10-value `economic_role` vocabulary ([D-195](#d-195)/[D-197](#d-197)) the emitted settlement row needs (`match_cities`/`CompatibilityMatrix` index by the 10-value set, not the 30-value one). + - **`headquarters_body` backfill reused the lifted heuristic, with one fix found live.** `most_populated_body_in_system` (lifted from the retired cross-reference function into `atlas.py`, reused by both the backfill and the Standalone-settlement emission) needed a third tiebreak: when every body in a system ties at `population = 0` (found live — GJ 702B/Prometheus, all 12 bodies), a population-then-`body_id`-ascending sort can land on an `asteroid_belt`/`oort_cloud` (its naming convention's `-` sorts before letters) over a `planet` sibling — a type that structurally cannot host a settlement. A `body_type` preference (`planet` < `moon` < `gas_giant` < `asteroid_belt`/`oort_cloud`) was added between the two, engaging **only** among population ties. + - **CityTenant is a new schema link, not a repurposed one.** `corporations.headquarters_city_id` (nullable FK → `atlas_city_names.id`) is the "corp→city link" this record calls for — the OLD `atlas_city_names.corp_id` column is left in place (additive-safe, no `DROP COLUMN`) but is now permanently `NULL`: nothing writes it once the cross-reference insert is gone, and nothing ever read it downstream (confirmed: zero references in `city_context_reader.rs`/`attractor_matching.rs` — the Rust consumption side never depended on it). + - **The merge is free.** Standalone-HQ settlements are inserted as ordinary `atlas_city_names` rows (`populate_standalone_hq_settlements`, Phase B) — same schema, no corp-linkage marker — so `read_body_settlements` → `match_cities` (the "natural seam") needed **zero Rust changes**; a Standalone HQ competes for attractors purely on population/`settlement_class`, exactly like any pooled city (verified live: `cascade_for_body("GJ251c")` places "The Gate Corporation" alongside "Tributarium"/"Ruhr" through the unmodified pipeline). Two new Rust unit tests lock this invariant in (`city_context_reader.rs`, `attractor_matching.rs`). + - **Verified against a real, repeated `make regen-db`:** 0 duplicate `(body_id, name)` groups (was the whole bug); 155/155 wiki corps get `corp_specialization`/`hq_placement`; 154/155 get `headquarters_body` (the 1 gap is a pre-existing broken `headquarters system` reference on `sova-station-works.md`, unrelated to this record); 95 `Standalone` corps → 94 settlements emitted (same 1 gap) + 60 `CityTenant` corps → 58 links (2 unmatched: `prometheus-labs`/`kovalev-freight`, both on GJ702B-belt, which — even after the tiebreak fix — has **zero** authored city names anywhere in its system, a wiki content gap, not an import bug); deterministic across 3 consecutive runs. The 10 legacy DB-only corps seeded by the now-deleted `populate-corporations.sh` (`mvg`, `adams-ford`, `dsmc`, and the 7 Cygni-B "combines") have no wiki page to author `corp_specialization` from and are left with `corp_specialization`/`hq_placement`/`headquarters_body` NULL — a logged, non-fatal gap (they are load-bearing for 2,468 `brand_products` rows via `generate_brands`, which reads `corporations` directly and doesn't care about wiki-page existence, so their rows are kept, not deleted). +- **Amendment (T-1075, 2026-07-16 — population/class derivation clarified):** the phrase above, "the per-settlement spread is **carried down into** the city/settlement source data," is superseded by **derivation-at-import, not hand-authoring**: `import_economics` computes the per-city population spread from the authored `bodies.population` on **every** `regen-db` run, via a documented integer rank-size (Zipf, exponent 1) curve (`atlas.py::_zipf_population_spread`, constants named + commented — tuning the curve is a deliberate code change, not a data edit). There is no per-city population *source file* — only `bodies.population` (already-baked) is authored; the per-settlement split is 100% derived. The spread runs over the **corrected, merged pool** (post-T-1074: Standalone-HQ settlements are ordinary rows by the time this runs, so they get a real share of their body's population like any city, not a bolted-on afterthought). `settlement_class` defaults every pooled row to `PopulationBudget` (D-196); a small authored override TOML (`wiki/economics/settlement_name_locked.toml`, `[[hero]]` stanzas keyed on `(body_id, name)` — not the volatile numeric id) pins 8 hero settlements to `NameLocked`. **EconomicTriggered and OrganicGrowth stay simulation-time** (D-196) — out of bake scope; so is the D-196 *generation-time* consumer that reads the baked population against the >=50k-active/<5k-ghost thresholds to decide skeleton depth (confirmed live: no such consumer exists yet anywhere in `server/src/atlas` — this record's bake only writes the values a future ticket will read). +- **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, extended here from 27 to 30 values), [D-195](#d-195)/[D-197](#d-197) (the 10-value `economic_role` vocabulary `standalone_economic_role` maps onto), [D-196](#d-196) (`SettlementClass` — the bake target), [D-010](#d-010) (determinism — only position is seed-derived). - **Dissent:** None ---