diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 56b0596b1..53539a2af 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**, **general professional/advisory**, or **licensed multi-site clinical** work — moving or selling what someone else made, or delivering certified services, not producing goods — which none of the 27 values name. Four values were **added** to `specialization_vocabulary.toml` (all `NonPhysical` → `CityTenant`): `trade_distribution`, `hospitality_hub`, `professional_services`, and `licensed_clinical_services` (the fourth via PR #177 review M1 — Somatic Futures is a 40+-clinic *licensed network*, one of several certified operators: neither generic consulting nor `longevity_monopoly`, which is Prometheus, THE sole facility). The vocabulary is now 31 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 4 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 corp pages are part of the `import_economics` meta-stamp source set (PR #177 T1): a frontmatter edit without a regen trips the stamp check like any TOML edit. + - **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 31), defaulting to the `NonPhysical` rule but reviewable/overridable per value without a code change. Standalone stanzas also carry `standalone_economic_role`, mapping the 31-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 31-value one). + - **`headquarters_body` is recomputed from source on every run — never kept from prior DB state (PR #177 H1/H2).** The importer-owned `corporations` columns (`corp_specialization`, `hq_placement`, `headquarters_body`) are NULL-reset and re-derived each run: `corporations` is append-only, so a keep-if-set guard would freeze the first run's output forever and silently promote DB state to source (exactly the asset-pipeline golden-rule inversion — the review caught the original one-shot backfill doing precisely that). An authored `headquarters_body:` frontmatter override wins where present (hard-validated: body must exist and sit in the corp's HQ system; none authored yet); otherwise `most_populated_body_in_system` (`atlas.py`, descended from the retired cross-reference function's heuristic) derives it with a four-stage deterministic tiebreak: **population DESC → city-bearing body (has `atlas_city_names` rows) → body-type rank (`planet` < `moon` < `gas_giant` < belt/oort) → `body_id`**. The city-presence tier is what the review's H2 added — the first draft ranked body-type directly and rested on a false premise ("belts can't host settlements"): belts in this world *do* host them (GJ845-belt: Orkney Ceramics; GJ268-belt: Jeju Lattice; GJ222A-belt: two Standalone HQs), so presence keeps settlements where they are, and the type rank only fires in fully cityless, fully unpopulated systems (GJ 702B — 12 bodies, zero population, zero city rows → planet `GJ702Bb` instead of the belt the old alphabetical sort picked). The presence set is read at step 7b, *before* the run's pool rebuild — i.e. the previous run's settled state, a deliberate, documented hysteresis (settlement continuity across regens) whose fixpoint is stable: each run's output reproduces the presence distribution it read. + - **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 real, repeated `make regen-db` runs — each run a full recompute, not a no-op (wording corrected per PR #177 H3; the pre-review text described the one-shot backfill being stably inert):** 0 duplicate `(body_id, name)` groups (was the whole bug); 155/155 wiki corps get `corp_specialization`/`hq_placement`; **154/155 `headquarters_body` re-derived on every run** (the 1 gap is a pre-existing broken `headquarters system` reference on `sova-station-works.md`, unrelated to this record), with two consecutive runs producing **zero differences** across all 165 corps' placement columns and tenancy targets; against the prior committed snapshot the recompute changes **exactly 2 placements** — `prometheus-labs`/`kovalev-freight`, `GJ702B-belt` → `GJ702Bb` (the intended H2 outcome) — and **zero tenancy targets** (`calluna-wellness` → (GJ845-belt, Orkney Ceramics) and `namsan-collective` → (GJ268-belt, Jeju Lattice) preserved via the city-presence tier). 95 `Standalone` corps → 94 settlements emitted (same 1 `sova` gap) + 60 `CityTenant` corps → 58 links (2 unmatched: the GJ 702B pair — now correctly on planet `GJ702Bb`, which still has zero authored city names, like every body in that system: a wiki content gap [T-1115], logged, not an import error). A poisoned-copy test confirmed no DB state survives the reset: hand-written garbage in all three columns is wiped and re-derived (wiki corp) or reverted to NULL (legacy corp with no authored source — the removed-key case). 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 reset to NULL across the board each run — 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 31 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 --- diff --git a/server/data/systems-schema.sql b/server/data/systems-schema.sql index 8fb0c4466..c4654ea65 100644 --- a/server/data/systems-schema.sql +++ b/server/data/systems-schema.sql @@ -238,8 +238,11 @@ CREATE TABLE IF NOT EXISTS corporations ( corp_type TEXT NOT NULL, -- corporation, combine, syndic, institution, independent scope TEXT, -- reach-wide, sector, system, local headquarters_system TEXT REFERENCES star_systems(system_id), - headquarters_body TEXT, -- body_id or station_id - specialization TEXT, -- primary product/service + headquarters_body TEXT, -- body_id or station_id (D-242: backfilled at import for + -- every corp via the most-populated-body-in-system heuristic + -- when not authored; see populate_corp_specialization) + specialization TEXT, -- free-text primary product/service (legacy prose; read by + -- generate_corporations for fuzzy brand relevance — left as-is) parent_corp TEXT REFERENCES corporations(corp_id), notes TEXT, @@ -248,6 +251,19 @@ CREATE TABLE IF NOT EXISTS corporations ( supply_chain_role TEXT, shadow_economy_access INTEGER DEFAULT 0, + -- D-242: controlled-vocabulary corp specialization (HQ-placement key) + its + -- baked placement outcome. corp_specialization is the corp_specialization + -- controlled vocabulary (reuses the D-237 27-value specialization_vocabulary + -- id-space — see wiki/economics/corp_hq_placement.toml header). hq_placement + -- is 'CityTenant' | 'Standalone', derived once at import from corp_specialization + -- via the authored placement map; NULL until populate_corp_specialization runs. + -- headquarters_city_id is the D-242 "corp -> city link" for CityTenant HQs + -- (many corps may tenant one city); NULL for Standalone HQs (which instead + -- get their own atlas_city_names row, cross-referenced by headquarters_body). + corp_specialization TEXT, + hq_placement TEXT, + headquarters_city_id INTEGER REFERENCES atlas_city_names(id), + updated_at TEXT DEFAULT (datetime('now')) ); @@ -493,6 +509,10 @@ CREATE INDEX IF NOT EXISTS idx_atlas_mountain_ranges_body ON atlas_mountain_rang -- City name reservations — replaces authored city positions in markers.json (D-207, #902) -- Position is generated by the city placement algorithm; name is authored or LLM-generated. +-- D-242: a Standalone corp HQ IS a settlement, so it gets an ordinary row here +-- (populate_standalone_hq_settlements) — same shape as a wiki-pooled city, no +-- special kind/flag. A CityTenant HQ is NOT a row here; its corp->city link +-- lives on corporations.headquarters_city_id (the inverse FK) instead. CREATE TABLE IF NOT EXISTS atlas_city_names ( id INTEGER PRIMARY KEY AUTOINCREMENT, body_id TEXT NOT NULL REFERENCES bodies(body_id) ON DELETE CASCADE, @@ -500,8 +520,15 @@ CREATE TABLE IF NOT EXISTS atlas_city_names ( kind TEXT NOT NULL DEFAULT 'city', -- 'capital' | 'city' economic_role TEXT NOT NULL, population INTEGER NOT NULL, - settlement_class TEXT, -- D-196 SettlementClass variant; NULL until placement - corp_id TEXT REFERENCES corporations(corp_id), -- nullable, corp HQ if applicable + settlement_class TEXT, -- D-196 SettlementClass variant; baked at import (D-242/T-1075) + corp_id TEXT REFERENCES corporations(corp_id), -- SUPERSEDED (D-242): the D-207/D-223 corp-HQ + -- cross-reference insert that populated this is + -- removed (it produced duplicate co-named "cities" — + -- one row per corp HQ, no UNIQUE(body_id, name)). + -- Always NULL going forward; the corp<->settlement + -- relationship now lives on corporations + -- (headquarters_body / headquarters_city_id). Column + -- kept (not dropped) — additive-safe, no live reader. reserved INTEGER NOT NULL DEFAULT 0, -- 1 = reserved for authored scenario use updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); diff --git a/server/data/systems.db b/server/data/systems.db index 93ffd0844..3080f69a6 100644 Binary files a/server/data/systems.db and b/server/data/systems.db differ diff --git a/server/src/atlas/attractor_matching.rs b/server/src/atlas/attractor_matching.rs index a31f252fe..1b00c7464 100644 --- a/server/src/atlas/attractor_matching.rs +++ b/server/src/atlas/attractor_matching.rs @@ -828,6 +828,76 @@ mod tests { assert!(p2.synthetic); } + /// D-242: "hubs are the significant cities, standalone HQs are typically + /// minor nodes." `match_cities` has no concept of "corp-originated" + /// settlement — a Standalone-HQ CityRecord competes on population/class + /// exactly like an ordinary pooled city, so a modest-population HQ must + /// NOT out-rank a genuinely major city for the best attractor, and a + /// major HQ (large population, or NameLocked) DOES win Tier A on the same + /// terms any other settlement would. Both directions verified here. + #[test] + fn standalone_hq_settlement_competes_on_equal_terms_with_pooled_cities() { + let major_city = make_city(1, SettlementClass::PopulationBudget, 5_000_000); + // A "minor node" Standalone HQ (D-242's own framing) — modest + // population, ordinary PopulationBudget class, nothing marking it as + // corp-originated (mirrors populate_standalone_hq_settlements' output + // shape: an atlas_city_names row with no corp_id, same as any city). + let minor_hq = make_city(2, SettlementClass::PopulationBudget, 80_000); + let cities = vec![major_city, minor_hq]; + let attractors = vec![ + make_attractor(5, 5, AttractorType::RiverMouth, 95), // best + make_attractor(10, 10, AttractorType::ValleyFloor, 30), // worst + ]; + let matrix = uniform_matrix(); + let placements = match_cities( + &cities, + &attractors, + &matrix, + None, + 512, + 256, + &TerritorialStatus::FrontierUnclaimed, + SeedChain::root(42), + ); + let major = placements.iter().find(|p| p.city_id == 1).unwrap(); + let minor = placements.iter().find(|p| p.city_id == 2).unwrap(); + assert_eq!( + major.position, + (5, 5), + "the major city (5M pop) must win the best attractor over an 80k-pop HQ" + ); + assert_eq!( + minor.position, + (10, 10), + "the minor HQ gets whatever's left — no special corp-origin priority" + ); + + // Flip it: a NameLocked HQ (the T-1075 hero-pin path — e.g. "The Gate + // Corporation" in the real data) DOES win Tier A, on the same + // NameLocked-priority terms as any hero-pinned city (tier_a_gets_priority, + // above) — population size never mattered for NameLocked, corp-origin + // doesn't add or subtract anything either. + let hero_hq = make_city(3, SettlementClass::NameLocked, 200_000); + let ordinary_city = make_city(4, SettlementClass::PopulationBudget, 3_000_000); + let cities2 = vec![hero_hq, ordinary_city]; + let placements2 = match_cities( + &cities2, + &attractors, + &matrix, + None, + 512, + 256, + &TerritorialStatus::FrontierUnclaimed, + SeedChain::root(42), + ); + let hero = placements2.iter().find(|p| p.city_id == 3).unwrap(); + assert_eq!( + hero.position, + (5, 5), + "NameLocked HQ wins Tier A over a larger ordinary city — same rule as any NameLocked settlement" + ); + } + #[test] fn all_cities_placed() { let cities: Vec = (1..=5) diff --git a/server/src/atlas/city_context_reader.rs b/server/src/atlas/city_context_reader.rs index 9ed3bee95..0cd2ee12e 100644 --- a/server/src/atlas/city_context_reader.rs +++ b/server/src/atlas/city_context_reader.rs @@ -1154,6 +1154,65 @@ mod tests { ); } + // ─── D-242 corp-HQ settlement model (T-1074/T-1075) ────────────────────── + // + // A Standalone-HQ settlement (economy_import/corporations.py: + // populate_standalone_hq_settlements) is inserted into atlas_city_names as + // an ORDINARY row — same schema, same fields, no corp linkage marker (the + // corp<->settlement relationship lives on corporations.headquarters_body, + // not on atlas_city_names). This module's test fixture schema + // (make_settlements_db, above) never had a corp_id column to begin with — + // this test makes that design invariant explicit: read_body_settlements + // (the D-211 placement pipeline's input) cannot distinguish a + // corp-Standalone-HQ row from a wiki-pooled city, by construction. If a + // future change ever needs to special-case corp-originated settlements, + // it will have to be threaded through explicitly — this test fails the + // moment that stops being true silently. + + #[test] + fn read_body_settlements_treats_standalone_hq_row_identically_to_pooled_city() { + // "Gate Corporation" here stands in for a Phase-B-inserted + // Standalone-HQ row (name = corp proper_name, economic_role from + // corp_hq_placement.toml's standalone_economic_role, population from + // the T-1075 Zipf bake, settlement_class from the same bake/override + // path as any other city) — indistinguishable in shape from the + // ordinary pooled cities "Tributarium"/"Ruhr" it sits alongside. + let db = make_settlements_db(&[ + ( + "Tributarium", + "manufacturing", + 2_727_273_224, + Some("PopulationBudget"), + ), + ( + "Ruhr", + "manufacturing", + 1_363_636_611, + Some("PopulationBudget"), + ), + ( + "Gate Corporation", + "manufacturing", + 909_090_165, + Some("NameLocked"), + ), + ]); + let reader = CityContextReader::open(&db).expect("open"); + let cities = reader.read_body_settlements("PlanetX").expect("read"); + assert_eq!(cities.len(), 3, "all three rows read back, HQ or not"); + + let hq = cities + .iter() + .find(|c| c.name == "Gate Corporation") + .expect("Standalone-HQ row must be present"); + // Every CityRecord field the D-211 pipeline reads is populated exactly + // like an ordinary city's — nothing marks this row as corp-originated. + assert_eq!(hq.economic_role, "manufacturing"); + assert_eq!(hq.population, 909_090_165); + assert_eq!(hq.settlement_class, SettlementClass::NameLocked); + assert!(!hq.is_capital, "Standalone HQ is not a capital by default"); + } + // ─── read_body_city_names (T-949) ──────────────────────────────────────── #[test] diff --git a/tooling/economy-db/economy_import/atlas.py b/tooling/economy-db/economy_import/atlas.py index 2a4be95f5..7781f4cfd 100644 --- a/tooling/economy-db/economy_import/atlas.py +++ b/tooling/economy-db/economy_import/atlas.py @@ -1,12 +1,24 @@ -"""Atlas index ownership (D-223, #951): canonical atlas_* schema, the -names-only city pool, and the corp-HQ city cross-reference (D-207).""" +"""Atlas index ownership (D-223, #951): canonical atlas_* schema and the +names-only city pool. + +The corp-HQ city cross-reference (D-207, `populate_atlas_city_names_corps`) +was REMOVED here (D-242, T-1074) — it inserted one atlas_city_names row per +corp HQ with no UNIQUE(body_id, name), producing duplicate co-named "cities" +(e.g. 10 Groombridge rows on GJ380c). The corp<->settlement relationship now +lives with the corp (economy_import/corporations.py:populate_corp_specialization), +not in the city pool. `most_populated_body_in_system` below is the one piece +of that removed function's logic still needed (by the D-242 replacement). + +`populate_settlement_population_class` (D-242, T-1075) bakes the per-settlement +population spread + settlement_class onto atlas_city_names — see its own +docstring for the rank-size derivation and D-196 class-assignment rule.""" import glob import json -import re import sqlite3 +import tomllib -from .paths import CORPORATIONS_DIR, SCHEMA_SQL, WIKI_STAR_SYSTEMS +from .paths import SCHEMA_SQL, SETTLEMENT_NAME_LOCKED_TOML, WIKI_STAR_SYSTEMS # Atlas geometry index tables (D-191). These hold computed positions — city # centres, road/river/rail polylines, ocean/mountain extents. Under D-223 the @@ -67,8 +79,9 @@ def populate_atlas_city_names(conn: sqlite3.Connection, dry_run: bool) -> int: - name : pooled city name - kind : 'city' — capital is chosen at placement (#955) - economic_role : inherited from bodies.economic_role; fallback 'mixed' - - population : 0 — assigned by the server cascade at placement (#955) - - corp_id : NULL — populated by populate_atlas_city_names_corps (#909) + - population : 0 — baked at import by populate_settlement_population_class (D-242, T-1075) + - corp_id : NULL — SUPERSEDED (D-242): the corp-HQ cross-reference + that used to populate this was removed; always NULL now - reserved : 0 markers.json is a names-only flavoured pool (D-223): it carries no geometry @@ -129,6 +142,16 @@ def populate_atlas_city_names(conn: sqlite3.Connection, dry_run: bool) -> int: print(f" warning: {len(unique)} body dirs not in DB — skipped: {unique[:5]}") if not dry_run: + # D-242: corporations.headquarters_city_id FK-references + # atlas_city_names(id). corporations is append-only (never cleared — + # db.clear_economics_tables doesn't touch it), so a PRIOR run's + # resolved CityTenant links survive as live FK references into the + # very rows this DELETE is about to remove — clear them first or the + # DELETE hits a FOREIGN KEY constraint failure on every run after the + # first (caught live in the T-1074/T-1075 regen-db integration test). + # Re-populated by corporations.populate_standalone_hq_settlements + # later in this same import run. + conn.execute("UPDATE corporations SET headquarters_city_id = NULL") conn.execute("DELETE FROM atlas_city_names") if rows: conn.executemany( @@ -141,110 +164,230 @@ def populate_atlas_city_names(conn: sqlite3.Connection, dry_run: bool) -> int: return len(rows) -def populate_atlas_city_names_corps(conn: sqlite3.Connection, dry_run: bool) -> tuple[int, int]: - """Cross-reference corp HQ city names into atlas_city_names (D-207, #909). +# body_type rank for the most_populated_body_in_system LAST-RESORT tiebreak — +# lower sorts first. This is NOT "belts can't host settlements" (they can and +# DO in this world: GJ845-belt hosts Orkney Ceramics, GJ268-belt hosts Jeju +# Lattice, GJ222A-belt hosts two Standalone-HQ settlements — PR #177 review +# H2 corrected the earlier false premise). It only engages when a system's +# bodies tie on BOTH population and city-presence — i.e. a fully cityless, +# fully unpopulated system, where NOTHING in the data distinguishes the +# siblings (found live: GJ 702B — 12 bodies, all population=0, zero city rows +# anywhere). There, a bare body_id sort lands on the belt purely because '-' +# (the naming convention for belts/oort clouds, e.g. "GJ702B-belt") sorts +# before letters; among equally blank siblings a planet is the saner default +# host for a future settlement than orbital rubble. +_BODY_TYPE_SETTLEMENT_PREFERENCE: dict[str, int] = { + "planet": 0, + "moon": 1, + "gas_giant": 2, + "asteroid_belt": 3, + "oort_cloud": 3, +} - For each corporation with a parseable headquarters field ("City (SYSTEM_ID)"): - - If atlas_city_names already has a row with matching name on a body in that - system: UPDATE the row to set corp_id. - - Otherwise: INSERT a reserved row (reserved=1) so the name is protected. - Attaches to the most-populated body in the system (fallback: any body). - Returns (n_updated, n_inserted). +def most_populated_body_in_system( + conn: sqlite3.Connection, system_id: str, city_bearing_bodies: set[str] +) -> str | None: + """Return the body_id in `system_id` that should host a corp HQ, or None + if the system has no bodies. + + D-242, T-1074: descended from the heuristic the retired + `populate_atlas_city_names_corps` (D-207, #909) used to attach a corp-HQ + row to a body; used by `populate_corp_specialization` + (economy_import/corporations.py) to resolve `corporations.headquarters_body` + on every run (PR #177 H1 — recompute, never treat prior DB output as + authored). + + Deterministic tiebreak, in order (D-010 #4; PR #177 H2): + (1) population descending — an authored population signal always wins; + (2) city-presence — a body in `city_bearing_bodies` (has atlas_city_names + rows) beats a cityless sibling. This is what keeps settlements where + they already are: a belt that hosts settlements stays the system's HQ + anchor (GJ845-belt/GJ268-belt tenancies), while a genuinely cityless + system falls through to (3); + (3) body_type rank (planet < moon < gas_giant < belt/oort — see + `_BODY_TYPE_SETTLEMENT_PREFERENCE`) — last resort among fully blank + siblings (GJ 702B → planet GJ702Bb); + (4) body_id ascending — final deterministic tiebreak. + + `city_bearing_bodies` is the caller's snapshot of `SELECT DISTINCT body_id + FROM atlas_city_names` taken BEFORE this run's pool rebuild (step 7b runs + before step 12's clear) — i.e. the PREVIOUS run's settled state. That is a + deliberate hysteresis: established settlements anchor future HQ placement + (settlement continuity across regens), and the fixpoint is stable — a + run's output presence distribution reproduces itself, so consecutive + regens compute identical values. See populate_corp_specialization's + docstring for the one-run-lag / fresh-DB caveats. """ - # Build system_id -> sorted bodies (by population desc, then body_id) - sys_bodies: dict[str, list[tuple[int, str, str]]] = {} - for body_id, sys_id, pop, role in conn.execute( - "SELECT body_id, system_id, COALESCE(population, 0), COALESCE(economic_role, 'mixed') FROM bodies" - ).fetchall(): - sys_bodies.setdefault(sys_id, []).append((pop, body_id, role)) - for v in sys_bodies.values(): - v.sort(key=lambda x: (-x[0], x[1])) + rows = conn.execute( + "SELECT body_id, body_type, COALESCE(population, 0) FROM bodies WHERE system_id = ?", + (system_id,), + ).fetchall() + if not rows: + return None + rows.sort( + key=lambda r: ( + -r[2], # population descending + 0 if r[0] in city_bearing_bodies else 1, # city-bearing body wins + _BODY_TYPE_SETTLEMENT_PREFERENCE.get(r[1], 3), # body_type rank + r[0], # body_id ascending + ) + ) + return rows[0][0] - # Build (body_id, name_lower) -> id index for existing atlas_city_names rows - existing: dict[tuple[str, str], int] = {} - body_to_sys: dict[str, str] = { - r[0]: r[1] - for r in conn.execute("SELECT body_id, system_id FROM bodies").fetchall() + +# --------------------------------------------------------------------------- +# D-242 population/settlement_class bake (T-1075) +# --------------------------------------------------------------------------- + +# Zipf/rank-size curve constants (T-1075: "name + document the curve +# constants — tuning is a code change by design"). weight(rank) = +# ZIPF_WEIGHT_SCALE // rank, i.e. the classic Zipf's-law exponent s=1.0 +# (rank-2 city gets half of rank-1's weight, rank-3 a third, ...) expressed +# as an integer basis-weight — no floats anywhere in the derivation (D-010). +# ZIPF_WEIGHT_SCALE only needs to be large enough that //rank doesn't +# truncate the tail ranks to 0 before normalization; 100 pooled cities on the +# densest body (Groombridge, ~16 rows pre-T-1074-cleanup) is far under this. +ZIPF_WEIGHT_SCALE = 1_000_000 + + +def _zipf_population_spread(body_population: int, n_cities: int) -> list[int]: + """Split `body_population` across `n_cities` settlements via a Zipf/rank-size + curve (T-1075). Returns a list of `n_cities` integer populations, index 0 = + rank 1 (the largest), summing EXACTLY to `body_population` (any integer- + division remainder is added to rank 1, so the total never drifts — the + standard largest-remainder-to-the-leader technique, not silently dropped). + + `n_cities == 0` returns `[]`. `body_population <= 0` returns all zeros + (an authored-population gap; not this function's job to invent one, per + D-242's "only position stays seed-derived" — POPULATION is baked from + `bodies.population`, and an unauthored body simply bakes zero). + + Which of a body's `atlas_city_names` rows *is* "rank 1" (gets the largest + share) is the caller's business (see `populate_settlement_population_class` + — rank is assigned by ascending `id`, i.e. insertion order, D-010 + determinism; there is no population signal to rank by BEFORE this + function runs, which is the whole reason it exists). + """ + if n_cities <= 0: + return [] + if body_population <= 0: + return [0] * n_cities + + weights = [ZIPF_WEIGHT_SCALE // rank for rank in range(1, n_cities + 1)] + total_weight = sum(weights) + shares = [body_population * w // total_weight for w in weights] + remainder = body_population - sum(shares) + shares[0] += remainder # give the leftover to rank 1 (D-010: deterministic) + return shares + + +def populate_settlement_population_class(conn: sqlite3.Connection, dry_run: bool) -> dict: + """Bake per-settlement population + settlement_class onto atlas_city_names + (D-242, T-1075). + + MUST run after BOTH D-242/T-1074 steps (`populate_atlas_city_names` and + `corporations.populate_standalone_hq_settlements`) — "the spread runs over + the CORRECTED pool," i.e. Standalone-HQ settlement rows are included in + the per-body city count/rank BEFORE the spread runs, not bolted on after. + + Two passes: + + (1) Population — for every body with at least one atlas_city_names row, + split `bodies.population` (COALESCE to 0 for unauthored bodies) across + its cities via `_zipf_population_spread`. Rank 1 (the largest share) + is the row with the LOWEST atlas_city_names.id on that body (D-010: + deterministic — insertion order is the only signal available; there + is no pre-existing population to rank by, which is exactly the gap + this function closes). A body with population=0/NULL bakes every one + of its cities to population=0 (not an error — 6/273 populated bodies + currently have zero pooled cities and 9/276 city-bearing bodies + currently have zero authored population; both directions are + legitimate content gaps, not import failures). + + (2) settlement_class — every pooled city defaults to 'PopulationBudget' + (D-196; the >=50k-active/<5k-ghost thresholds are read from this baked + population at GENERATION time by a later, not-yet-built consumer — + out of this bake's scope, confirmed: no such consumer exists in + server/src/atlas yet). The authored NameLocked override list + (settlement_name_locked.toml, `[[hero]]` stanzas of body_id + name) + then overrides specific rows to 'NameLocked' — matched by + (body_id, name), NOT the volatile numeric id (see that TOML's header). + A stanza with no matching row is a non-fatal warning (a future + markers.json rename must not hard-fail regen-db). + + Returns a coverage dict: {"bodies_spread", "cities_populated", + "name_locked_applied", "name_locked_unmatched"}. + """ + # --- Pass 1: population spread ----------------------------------------- + body_population: dict[str, int] = { + r[0]: (r[1] or 0) for r in conn.execute("SELECT body_id, population FROM bodies").fetchall() } - for row_id, body_id, name in conn.execute( - "SELECT id, body_id, name FROM atlas_city_names" + # body_id -> [(city_id, ...)] ordered by id ascending (rank 1 = lowest id). + city_rows_by_body: dict[str, list[int]] = {} + for city_id, body_id in conn.execute( + "SELECT id, body_id FROM atlas_city_names ORDER BY body_id, id" ).fetchall(): - existing[(body_id, name.lower())] = row_id + city_rows_by_body.setdefault(body_id, []).append(city_id) - # Build system_id -> set of body_ids for quick lookup - sys_body_ids: dict[str, set[str]] = {} - for body_id, sys_id in body_to_sys.items(): - sys_body_ids.setdefault(sys_id, set()).add(body_id) + population_updates: list[tuple[int, int]] = [] # (population, city_id) + for body_id, city_ids in city_rows_by_body.items(): + pop = body_population.get(body_id, 0) + spread = _zipf_population_spread(pop, len(city_ids)) + for city_id, city_pop in zip(city_ids, spread): + population_updates.append((city_pop, city_id)) - updated: list[tuple[str, int]] = [] # (corp_id, atlas_row_id) - inserted: list[tuple] = [] # insert rows - - for corp_id, headquarters_system in conn.execute( - "SELECT corp_id, headquarters_system FROM corporations WHERE headquarters_system IS NOT NULL" - ).fetchall(): - # Retrieve original headquarters string from wiki to get city name - md_file = CORPORATIONS_DIR / f"{corp_id}.md" - if not md_file.exists(): - continue - hq_raw = "" - with open(md_file) as f: - in_fm = False - for line in f: - if line.strip() == "---": - if not in_fm: - in_fm = True - continue - else: - break - if in_fm and line.startswith("headquarters:"): - hq_raw = line.split(":", 1)[1].strip().strip('"') - break - if not hq_raw: - continue - m = re.search(r"\(([^)]+)\)", hq_raw) - city_name = hq_raw[: m.start()].strip() if m else hq_raw.strip() - if not city_name: - continue - - # Try to find a matching atlas_city_names row in the same system - body_ids_in_sys = sys_body_ids.get(headquarters_system, set()) - match_id: int | None = None - # sorted() for determinism: on a name collision across bodies in the - # same system, set iteration order is not stable (D-010 #4). - for body_id in sorted(body_ids_in_sys): - key = (body_id, city_name.lower()) - if key in existing: - match_id = existing[key] - break - - if match_id is not None: - updated.append((corp_id, match_id)) - else: - # Sol (system 'GJ 0') is exempt from the normal generators (D-223, - # #951) — do not synthesize a reserved corp-HQ row on a Sol body; - # Sol's atlas data comes from its own scripted integration. - if headquarters_system == "GJ 0": - continue - # Insert a reserved row on the most-populated body in the system - candidates = sys_bodies.get(headquarters_system, []) - if not candidates: - continue - _, target_body_id, body_role = candidates[0] - inserted.append((target_body_id, city_name, "city", body_role, 0, corp_id, 1)) + # --- Pass 2: settlement_class default + NameLocked override ------------ + with open(SETTLEMENT_NAME_LOCKED_TOML, "rb") as f: + hero_data = tomllib.load(f) + hero_entries: list[dict] = hero_data.get("hero", []) if not dry_run: - for corp_id, row_id in updated: - conn.execute( - "UPDATE atlas_city_names SET corp_id = ? WHERE id = ?", - (corp_id, row_id), - ) - if inserted: + if population_updates: conn.executemany( - """INSERT OR IGNORE INTO atlas_city_names - (body_id, name, kind, economic_role, population, corp_id, reserved) - VALUES (?, ?, ?, ?, ?, ?, ?)""", - inserted, + "UPDATE atlas_city_names SET population = ? WHERE id = ?", + population_updates, ) + # Default every row to PopulationBudget (D-196). Idempotent: a re-run + # always resets to the default before re-applying overrides below, so + # a stanza REMOVED from settlement_name_locked.toml correctly reverts + # its row to PopulationBudget rather than leaving a stale NameLocked. + conn.execute("UPDATE atlas_city_names SET settlement_class = 'PopulationBudget'") - return len(updated), len(inserted) + # NameLocked matching is a read-only SELECT count (safe under dry_run too + # — a --dry-run run must still be able to REPORT an unmatched hero stanza, + # not just skip detecting it; the WRITE is what's gated on dry_run). + name_locked_applied = 0 + name_locked_unmatched: list[str] = [] + for entry in hero_entries: + body_id = entry.get("body_id") + name = entry.get("name") + if not body_id or not name: + continue + match_count = conn.execute( + "SELECT COUNT(*) FROM atlas_city_names WHERE body_id = ? AND name = ?", + (body_id, name), + ).fetchone()[0] + if match_count == 0: + name_locked_unmatched.append(f"{body_id}/{name}") + continue + if not dry_run: + conn.execute( + "UPDATE atlas_city_names SET settlement_class = 'NameLocked' " + "WHERE body_id = ? AND name = ?", + (body_id, name), + ) + name_locked_applied += match_count + + if name_locked_unmatched: + print( + f" warning: {len(name_locked_unmatched)} NameLocked hero stanza(s) " + f"matched no atlas_city_names row: {name_locked_unmatched}" + ) + + return { + "bodies_spread": len(city_rows_by_body), + "cities_populated": len(population_updates), + "name_locked_applied": name_locked_applied, + "name_locked_unmatched": len(name_locked_unmatched), + } diff --git a/tooling/economy-db/economy_import/corporations.py b/tooling/economy-db/economy_import/corporations.py index 898d33c26..1bf88c389 100644 --- a/tooling/economy-db/economy_import/corporations.py +++ b/tooling/economy-db/economy_import/corporations.py @@ -1,10 +1,14 @@ -"""Corporation wiki parsing, D-182 sync, and corp_presence population.""" +"""Corporation wiki parsing, D-182 sync, corp_presence population, and the +D-242 corp-HQ specialization/placement bake (T-1074).""" import re import sqlite3 +import tomllib from pathlib import Path -from .paths import CORPORATIONS_DIR +from .atlas import most_populated_body_in_system +from .errors import ImportAborted +from .paths import CORP_HQ_PLACEMENT_TOML, CORPORATIONS_DIR # --------------------------------------------------------------------------- # Corporation wiki parsing @@ -51,12 +55,26 @@ def load_wiki_corps() -> list[dict]: hq = fm.get("headquarters", "") m = re.search(r"\(([^)]+)\)", hq) system_id = m.group(1) if m else None + # D-242, T-1074: authored corp_specialization frontmatter key — the + # HQ-placement key (reuses the D-237 specialization_vocabulary + # id-space, extended from 27 to 31 values for corp coverage; see + # wiki/economics/corp_hq_placement.toml). Optional — None until a + # corp's page is authored with the key. + # + # headquarters_body (PR #177 H1): optional authored body_id override. + # When present it pins corporations.headquarters_body directly (hard- + # validated at import: the body must exist AND sit in the corp's + # headquarters system); absent, the deterministic heuristic derives it + # on every run. Authored overrides live HERE (source), never in the DB + # — the asset-pipeline golden rule. No page authors it yet. corps.append({ "corp_id": fm["slug"], "proper_name": fm["title"], "system_id": system_id, "tags": fm.get("tags", []), "scope": fm.get("scope", ""), + "corp_specialization": fm.get("corp_specialization") or None, + "headquarters_body": fm.get("headquarters_body") or None, }) return corps @@ -173,6 +191,204 @@ def _resolve_hq_location( return None +# --------------------------------------------------------------------------- +# D-242 corp-HQ specialization + placement bake (T-1074) +# --------------------------------------------------------------------------- + + +def populate_corp_specialization( + conn: sqlite3.Connection, wiki_corps: list[dict], dry_run: bool +) -> dict: + """Phase A of the D-242 corp-HQ model (T-1074; reworked per PR #177 H1/H2/T2). + + RESET + DERIVE, every run. The three importer-owned columns on + `corporations` — `corp_specialization`, `hq_placement`, + `headquarters_body` — are NULLed for ALL corps first, then re-derived from + source. Nothing in those columns is ever treated as authored: `corporations` + is append-only (never cleared), so any keep-if-set guard would freeze the + FIRST run's output forever and silently promote DB state to source — the + exact inversion of the asset-pipeline golden rule (PR #177 H1: the old + fill-only-when-NULL backfill made the heuristic a permanent one-shot; its + own earlier output blocked every later fix from applying). The reset also + guarantees a frontmatter key REMOVED from a corp's page reverts that corp + to NULL instead of leaving a stale baked value (T2 — mirrors the + settlement_class reset in atlas.populate_settlement_population_class). + The fourth derived column, `headquarters_city_id`, follows the same + discipline at steps 12/13 (NULLed in populate_atlas_city_names, re-derived + in populate_standalone_hq_settlements). + + For every wiki corp: validate its authored `corp_specialization` + (frontmatter key, reuses the D-237 `specialization_vocabulary` id-space — + see wiki/economics/corp_hq_placement.toml header) against the vocabulary + table (must run AFTER `import_system_specialization`, which populates + `specialization_vocabulary`); resolve `hq_placement` ('CityTenant' | + 'Standalone') from `corp_hq_placement.toml`; and resolve + `headquarters_body`: an authored `headquarters_body:` frontmatter override + wins (hard-validated: the body must exist and sit in the corp's + headquarters system; no page authors one yet), else the deterministic + heuristic (`atlas.most_populated_body_in_system`) derives it with the + PR #177 H2 tiebreak — population DESC, then city-bearing body (has + atlas_city_names rows), then body-type rank, then body_id. + + **City-presence timing (H2):** the presence set is read here, at step 7b — + BEFORE step 12 clears/rebuilds the pool — so it reflects the PREVIOUS + run's settled state (pool cities + Standalone-HQ settlements). Deliberate + hysteresis: established settlements anchor HQ placement across regens + (GJ845-belt keeps Orkney Ceramics + calluna-wellness's tenancy), and the + fixpoint is stable — each run's output reproduces the presence + distribution it read, so consecutive regens derive identical values. Two + documented consequences: a brand-new Standalone corp's settlement only + influences OTHER corps' placement from the run after it first lands + (one-run lag, converges immediately); and a hypothetical from-empty DB + rebuild would see an inert presence tier on its first run (regen-db always + evolves the committed snapshot, so this does not arise in practice — if + the table is empty a loud warning prints rather than silently degrading). + + Must run AFTER `sync_corporations` (corp rows must exist) and BEFORE + `import_corp_presence` (which reads `headquarters_body` back off + `corporations`) and BEFORE step 12 (see presence timing above). + + HARD error (raises ImportAborted) on: a `corp_specialization` value not in + the vocabulary; a vocabulary id with no placement stanza; an invalid + placement value; or an authored `headquarters_body` override that names a + missing body or a body outside the corp's headquarters system. A corp with + NO authored `corp_specialization` is a soft warning (stays NULL across the + board — the 10 legacy DB-only corps have no wiki page); all 155 wiki corps + are authored as of T-1074. + + Returns a coverage dict: + {"total", "specialized", "hq_resolved", "hq_overridden"}. + """ + with open(CORP_HQ_PLACEMENT_TOML, "rb") as f: + placement_map: dict[str, dict] = tomllib.load(f) + + valid_spec_ids = { + r[0] for r in conn.execute("SELECT specialization_id FROM specialization_vocabulary").fetchall() + } + valid_body_system = { + r[0]: r[1] + for r in conn.execute("SELECT body_id, system_id FROM bodies").fetchall() + } + # H2 presence set — previous run's settled state (see docstring timing note). + city_bearing_bodies = { + r[0] for r in conn.execute("SELECT DISTINCT body_id FROM atlas_city_names").fetchall() + } + if not city_bearing_bodies: + print( + " warning: atlas_city_names is empty — the city-presence " + "tiebreak tier is inert this run (placements fall through to " + "body-type rank; they converge on the next run)" + ) + + errors: list[str] = [] + unauthored: list[str] = [] + + # (corp_id, corp_specialization, hq_placement, authored_hq_body_or_None). + spec_rows: list[tuple[str, str, str, str | None]] = [] + + for corp in wiki_corps: + corp_id = corp["corp_id"] + spec = corp.get("corp_specialization") + if not spec: + unauthored.append(corp_id) + continue + if spec not in valid_spec_ids: + errors.append( + f"{corp_id}: corp_specialization '{spec}' not in specialization_vocabulary " + f"(add it to wiki/economics/specialization_vocabulary.toml or fix the typo)" + ) + continue + placement_entry = placement_map.get(spec) + if not placement_entry or not placement_entry.get("hq_placement"): + errors.append( + f"{corp_id}: specialization '{spec}' has no hq_placement stanza in " + f"corp_hq_placement.toml (every specialization_vocabulary id needs one)" + ) + continue + placement = placement_entry["hq_placement"] + if placement not in ("CityTenant", "Standalone"): + errors.append( + f"{corp_id}: specialization '{spec}' hq_placement '{placement}' invalid " + f"(expected 'CityTenant' or 'Standalone')" + ) + continue + # Authored headquarters_body override (H1) — hard-validate: typos in + # freshly-authored data must fail loudly, not silently fall back. + authored_body = corp.get("headquarters_body") + if authored_body is not None: + if authored_body not in valid_body_system: + errors.append( + f"{corp_id}: authored headquarters_body '{authored_body}' is not a " + f"known bodies.body_id" + ) + continue + corp_system = corp.get("system_id") + if corp_system and valid_body_system[authored_body] != corp_system: + errors.append( + f"{corp_id}: authored headquarters_body '{authored_body}' sits in " + f"system '{valid_body_system[authored_body]}', not the corp's " + f"headquarters system '{corp_system}'" + ) + continue + spec_rows.append((corp_id, spec, placement, authored_body)) + + if errors: + print(f" CORP SPECIALIZATION ERRORS ({len(errors)}):") + for e in errors: + print(f" - {e}") + raise ImportAborted() + + if unauthored: + print( + f" warning: {len(unauthored)} corp(s) have no authored " + f"corp_specialization (stay NULL; no HQ placement): {unauthored[:5]}" + ) + + # Pre-compute the resolution so dry-run reports the same numbers a real + # run writes (write is gated on dry_run; the derivation is not). + n_resolved = 0 + n_overridden = 0 + resolved_rows: list[tuple[str, str, str, str | None]] = [] + for corp_id, spec, placement, authored_body in spec_rows: + if authored_body is not None: + resolved = authored_body + n_overridden += 1 + else: + hq_system_row = conn.execute( + "SELECT headquarters_system FROM corporations WHERE corp_id = ?", + (corp_id,), + ).fetchone() + hq_system = hq_system_row[0] if hq_system_row else None + resolved = ( + most_populated_body_in_system(conn, hq_system, city_bearing_bodies) + if hq_system + else None + ) + if resolved is not None: + n_resolved += 1 + resolved_rows.append((corp_id, spec, placement, resolved)) + + if not dry_run: + # T2/H1 reset: importer-owned columns are never source — wipe, then derive. + conn.execute( + "UPDATE corporations SET corp_specialization = NULL, " + "hq_placement = NULL, headquarters_body = NULL" + ) + for corp_id, spec, placement, resolved in resolved_rows: + conn.execute( + "UPDATE corporations SET corp_specialization = ?, hq_placement = ?, " + "headquarters_body = ? WHERE corp_id = ?", + (spec, placement, resolved, corp_id), + ) + + return { + "total": len(wiki_corps), + "specialized": len(spec_rows), + "hq_resolved": n_resolved, + "hq_overridden": n_overridden, + } + + def import_corp_presence( conn: sqlite3.Connection, wiki_corps: list[dict], @@ -236,3 +452,109 @@ def import_corp_presence( ) return len(rows) + + +def populate_standalone_hq_settlements(conn: sqlite3.Connection, dry_run: bool) -> dict: + """Phase B of the D-242 corp-HQ model (T-1074). + + MUST run after `populate_atlas_city_names` (D-223, atlas.py) — that step's + `DELETE FROM atlas_city_names` at the top of its own run means Phase B is + always writing into a freshly-cleared, freshly-repopulated pool, so this + function is the SOLE remaining writer for corp-HQ rows on every run: no + idempotency guard is needed (nothing from a prior run can survive to + collide with this one). Also must run after `populate_corp_specialization` + (Phase A — needs corp_specialization/hq_placement/headquarters_body set). + + Two independent passes over `corporations` rows with + `hq_placement IS NOT NULL AND headquarters_body IS NOT NULL`: + + - Standalone: INSERT one new atlas_city_names row per corp — a company-town + settlement named after the corp (`proper_name`), on `headquarters_body`, + with `economic_role` from `corp_hq_placement.toml`'s + `standalone_economic_role` (the existing 10-value D-195/D-197 vocabulary + — NOT the 31-value specialization_vocabulary, which `match_cities`/ + `CompatibilityMatrix` do not index). `population = 0` — T-1075 bakes the + per-settlement population spread over the corrected pool (this row + included) in a later step; this function only creates the row. + `settlement_class` is left NULL here too, for the same reason (T-1075 + assigns it from the baked population). + - CityTenant: resolve `corporations.headquarters_city_id` to an existing + atlas_city_names row on `headquarters_body` — the lowest `id` (D-010: + deterministic, no population signal exists yet to rank by; population is + baked in a later step by T-1075). A body with a Standalone-only pool (no + ordinary city — e.g. a purely industrial body with zero wiki-pooled + names) has nothing to tenant; the corp is logged and left untenanted + rather than erroring — a corp with no city to join on its own HQ body is + a content gap (an empty markers.json city pool for that body), not an + import-time bug. + + Returns a coverage dict: {"standalone_inserted", "tenant_linked", "tenant_unmatched"}. + """ + with open(CORP_HQ_PLACEMENT_TOML, "rb") as f: + placement_map: dict[str, dict] = tomllib.load(f) + + rows = conn.execute( + """SELECT corp_id, proper_name, hq_placement, headquarters_body, corp_specialization + FROM corporations + WHERE hq_placement IS NOT NULL AND headquarters_body IS NOT NULL""" + ).fetchall() + + valid_body_ids = {r[0] for r in conn.execute("SELECT body_id FROM bodies").fetchall()} + + standalone_inserts: list[tuple] = [] # (body_id, name, kind, economic_role, population) + tenant_candidates: list[tuple[str, str]] = [] # (corp_id, headquarters_body) + unmatched_bodies: list[str] = [] + + for corp_id, proper_name, placement, hq_body, spec in rows: + if hq_body not in valid_body_ids: + unmatched_bodies.append(f"{corp_id} (headquarters_body '{hq_body}' not in bodies)") + continue + if placement == "Standalone": + role = (placement_map.get(spec) or {}).get("standalone_economic_role") or "manufacturing" + standalone_inserts.append((hq_body, proper_name, "city", role, 0)) + elif placement == "CityTenant": + tenant_candidates.append((corp_id, hq_body)) + + if not dry_run and standalone_inserts: + conn.executemany( + """INSERT INTO atlas_city_names (body_id, name, kind, economic_role, population) + VALUES (?, ?, ?, ?, ?)""", + standalone_inserts, + ) + n_inserted = len(standalone_inserts) + + # Tenant-matching is a read-only SELECT (safe under dry_run too — a + # --dry-run run must still be able to REPORT the "no city to tenant on + # this body" case, not just skip detecting it; the WRITE is what's gated + # on dry_run, not the check). + n_linked = 0 + n_unmatched = 0 + unmatched_corps: list[str] = [] + for corp_id, hq_body in tenant_candidates: + city_row = conn.execute( + "SELECT id FROM atlas_city_names WHERE body_id = ? ORDER BY id LIMIT 1", + (hq_body,), + ).fetchone() + if city_row is None: + unmatched_corps.append(f"{corp_id} (no city on body '{hq_body}' to tenant)") + n_unmatched += 1 + continue + if not dry_run: + conn.execute( + "UPDATE corporations SET headquarters_city_id = ? WHERE corp_id = ?", + (city_row[0], corp_id), + ) + n_linked += 1 + + if unmatched_bodies: + print(f" warning: {len(unmatched_bodies)} corp(s) skipped (bad headquarters_body): " + f"{unmatched_bodies[:5]}") + if unmatched_corps: + print(f" warning: {len(unmatched_corps)} CityTenant corp(s) unmatched: " + f"{unmatched_corps[:5]}") + + return { + "standalone_inserted": n_inserted, + "tenant_linked": n_linked, + "tenant_unmatched": n_unmatched, + } diff --git a/tooling/economy-db/economy_import/migration.py b/tooling/economy-db/economy_import/migration.py index 308023cf7..4881baa6c 100644 --- a/tooling/economy-db/economy_import/migration.py +++ b/tooling/economy-db/economy_import/migration.py @@ -345,6 +345,9 @@ COLUMN_MIGRATIONS: list[tuple[str, str, str]] = [ ("atlas_city_names", "settlement_class", "TEXT"), # D-196 — NULL until placement (#37) ("system_economy", "economic_specialization", "TEXT"), # D-237 — authored specialization layer ("system_economy", "cultural_specialization", "TEXT"), # D-237 — authored specialization layer + ("corporations", "corp_specialization", "TEXT"), # D-242 — HQ-placement key (reused D-237 vocab) + ("corporations", "hq_placement", "TEXT"), # D-242 — 'CityTenant' | 'Standalone', baked at import + ("corporations", "headquarters_city_id", "INTEGER REFERENCES atlas_city_names(id)"), # D-242 CityTenant link ] diff --git a/tooling/economy-db/economy_import/paths.py b/tooling/economy-db/economy_import/paths.py index 00c46d3ff..fb9557227 100644 --- a/tooling/economy-db/economy_import/paths.py +++ b/tooling/economy-db/economy_import/paths.py @@ -13,9 +13,12 @@ from generator_sources import ( ARCHITECTURE_TRAIT_CATALOG_TOML, ARCHITECTURE_ZONE_BIAS_TOML, COLOR_REGISTER_BANDS_TOML, + CORP_HQ_PLACEMENT_TOML, + CORPORATIONS_DIR, GENERATE_BRANDS_WRAPPER, OBJECT_TAG_VOCABULARY_TOML, REPO_ROOT, + SETTLEMENT_NAME_LOCKED_TOML, SPECIALIZATION_VOCAB_TOML, SYSTEM_SPECIALIZATION_TOML, ) @@ -29,6 +32,7 @@ __all__ = [ "COLOR_REGISTER_BANDS_TOML", "COMMODITIES_TOML", "CORPORATIONS_DIR", + "CORP_HQ_PLACEMENT_TOML", "CURRENCY_ZONES_TOML", "DB_PATH", "GENERATED_BRANDS_TOML", @@ -36,6 +40,7 @@ __all__ = [ "OBJECT_TAG_VOCABULARY_TOML", "REPO_ROOT", "SCHEMA_SQL", + "SETTLEMENT_NAME_LOCKED_TOML", "SPECIALIZATION_VOCAB_TOML", "STAR_MAP", "SYSTEM_SPECIALIZATION_TOML", @@ -48,7 +53,9 @@ COMMODITIES_TOML: Path = REPO_ROOT / "wiki" / "economics" / "commodities.toml" CHAINS_TOML: Path = REPO_ROOT / "wiki" / "economics" / "production_chains.toml" CURRENCY_ZONES_TOML: Path = REPO_ROOT / "wiki" / "economics" / "currency_zones.toml" SCHEMA_SQL: Path = REPO_ROOT / "server" / "data" / "systems-schema.sql" -CORPORATIONS_DIR: Path = REPO_ROOT / "wiki" / "corporations" +# CORPORATIONS_DIR moved to generator_sources.py (T1, PR #177): the corp pages +# are now part of the stamped source set, and stamped paths are defined once +# in the registry — re-exported via the import block above. WIKI_STAR_SYSTEMS: Path = REPO_ROOT / "wiki" / "star-systems" BRANDS_TOML: Path = REPO_ROOT / "wiki" / "economics" / "corporations" / "brands.toml" GENERATED_BRANDS_TOML: Path = ( diff --git a/tooling/economy-db/import_economics.py b/tooling/economy-db/import_economics.py index 51e6eb031..cc05b274d 100755 --- a/tooling/economy-db/import_economics.py +++ b/tooling/economy-db/import_economics.py @@ -9,7 +9,11 @@ Reads TOML/JSON source files and populates the economics tables: - currency_zone on star_systems (default TRACTUS_PRIMARY) - gate_energy_connected on star_systems (D-186: false for MARK_PRIMARY zones) - corporations from wiki/corporations/*.md (sync + insert new records) + - corp_specialization/hq_placement/headquarters_body/headquarters_city_id + on corporations (D-242: HQ-placement key + baked + CityTenant link, from corp_hq_placement.toml) - corp_presence from wiki/corporations/*.md (headquarters location data) + - atlas_city_names Standalone-HQ settlement rows (D-242, T-1074) Validation (hard errors, non-zero exit on any failure): - Wiki corporation names must match DB proper_name records (D-182 sync constraint) @@ -178,6 +182,22 @@ def main() -> None: n_db_corps = conn.execute("SELECT COUNT(*) FROM corporations").fetchone()[0] print(f" {n_db_corps} corporations in DB ({len(wiki_corps)} from wiki)") + # 7b. Corp specialization + HQ placement (D-242, T-1074) — Phase A. + # Reset + derive on every run (PR #177 H1/T2): the importer-owned + # corporations columns are NULLed and re-derived from source, never + # kept from a prior run. Must run after corp sync (rows must exist), + # before corp_presence (which reads headquarters_body back off + # corporations), and before step 12 (the H2 city-presence tiebreak + # reads atlas_city_names BEFORE this run's clear/rebuild — the + # previous run's settled state, by design). + print(" [7b/10] Importing corp specialization + HQ placement (D-242)...") + corp_spec = corporations.populate_corp_specialization(conn, wiki_corps, args.dry_run) + print( + f" {corp_spec['specialized']}/{corp_spec['total']} corps specialized, " + f"{corp_spec['hq_resolved']} headquarters_body resolved (recomputed every run; " + f"{corp_spec['hq_overridden']} authored overrides)" + ) + # 8. Corp presence from wiki headquarters data print(" [8/10] Importing corp presence...") commodity_ids = { @@ -208,10 +228,32 @@ def main() -> None: n_cities = atlas.populate_atlas_city_names(conn, args.dry_run) print(f" {n_cities} city name rows") - # 13. atlas_city_names corp HQ cross-reference (D-207, #909) - print(" [13/13] Cross-referencing corp HQ cities into atlas_city_names...") - n_updated, n_inserted = atlas.populate_atlas_city_names_corps(conn, args.dry_run) - print(f" {n_updated} rows updated, {n_inserted} reserved rows inserted") + # 13. Standalone-HQ settlements + CityTenant city links (D-242, T-1074) — Phase B. + # SUPERSEDES the retired corp-HQ cross-reference (D-207, #909) — that + # step inserted one atlas_city_names row per corp HQ with no + # UNIQUE(body_id, name), producing duplicate co-named "cities" (10 + # Groombridge rows on GJ380c). Must run after populate_atlas_city_names + # (the city pool Standalone HQs join and CityTenant HQs tenant) and + # after step 7b (corp_specialization/hq_placement/headquarters_body). + print(" [13/13] Emitting Standalone-HQ settlements + CityTenant links (D-242)...") + hq_settlements = corporations.populate_standalone_hq_settlements(conn, args.dry_run) + print( + f" {hq_settlements['standalone_inserted']} Standalone-HQ settlements, " + f"{hq_settlements['tenant_linked']} CityTenant links " + f"({hq_settlements['tenant_unmatched']} unmatched)" + ) + + # 13b. Per-settlement population + settlement_class bake (D-242, T-1075). + # Runs over the CORRECTED pool — after both T-1074 steps, so + # Standalone-HQ settlements are included in the rank-size spread, not + # bolted on after. + print(" [13b/13] Baking settlement population + settlement_class (D-242)...") + pop_bake = atlas.populate_settlement_population_class(conn, args.dry_run) + print( + f" {pop_bake['cities_populated']} cities populated across " + f"{pop_bake['bodies_spread']} bodies, {pop_bake['name_locked_applied']} " + f"NameLocked pins applied" + ) # 14. Architecture-flavor trait templates (D-232, #993). Catalog first, # then sparse per-body hero bias (FK -> trait_templates + bodies). diff --git a/tooling/generator_sources.py b/tooling/generator_sources.py index 453352743..647394427 100644 --- a/tooling/generator_sources.py +++ b/tooling/generator_sources.py @@ -74,6 +74,24 @@ SPECIALIZATION_VOCAB_TOML: Path = ( SYSTEM_SPECIALIZATION_TOML: Path = ( REPO_ROOT / "wiki" / "economics" / "system_specialization.toml" ) +# D-242 corp-HQ settlement model (T-1074): specialization -> {CityTenant, +# Standalone} placement map, keyed on the reused D-237 vocabulary above. +CORP_HQ_PLACEMENT_TOML: Path = ( + REPO_ROOT / "wiki" / "economics" / "corp_hq_placement.toml" +) +# The corp wiki pages themselves (PR #177 review T1): their frontmatter +# (corp_specialization, headquarters, optional headquarters_body) now shapes +# the DB — specialization drives hq_placement, headquarters_body, and the +# Standalone-HQ settlement rows in atlas_city_names — so a page edit without a +# regen must trip the stamp check exactly like a TOML edit does. Defined here +# (not in economy_import/paths.py) per the stamped-paths-live-here convention; +# paths.py re-exports it. +CORPORATIONS_DIR: Path = REPO_ROOT / "wiki" / "corporations" +# D-242 population/settlement_class bake (T-1075): the small authored +# NameLocked hero-city override list. +SETTLEMENT_NAME_LOCKED_TOML: Path = ( + REPO_ROOT / "wiki" / "economics" / "settlement_name_locked.toml" +) # D-232 architecture-flavor trait-template catalog + sparse hero bias (#993). ARCHITECTURE_TRAIT_CATALOG_TOML: Path = ( REPO_ROOT / "wiki" / "economics" / "architecture_trait_catalog.toml" @@ -117,14 +135,38 @@ def _economy_import_modules() -> tuple[Path, ...]: return modules +def _corporation_pages() -> tuple[Path, ...]: + """All corp wiki pages the importer reads, collected by glob (T1, PR #177). + + `load_wiki_corps` (economy_import/corporations.py) reads every + wiki/corporations/*.md EXCEPT index.md — the frontmatter feeds + corp_specialization/hq_placement/headquarters_body and the Standalone-HQ + settlement rows, so the read-set is stamped with the same glob-not-list + + fail-closed discipline as `_economy_import_modules`. index.md is excluded + because the importer skips it by name: stamping it would flag a false + stale on edits that cannot change DB output. + """ + pages = tuple( + sorted(p for p in CORPORATIONS_DIR.glob("*.md") if p.name != "index.md") + ) + if not pages: + raise RuntimeError( + f"corporation pages not found at {CORPORATIONS_DIR} — " + "the import_economics stamp source set would be incomplete" + ) + return pages + + # Canonical source set for import_economics' meta stamp. Covers the Python # entrypoint and its module package, the Rust binary it invokes (generate_brands # main.rs + names.rs + surname corpus + wrapper script), the shared schema -# version constant, the authored data TOMLs, and this registry itself. +# version constant, the authored data TOMLs, the corp wiki pages whose +# frontmatter the importer bakes (T1, PR #177), and this registry itself. IMPORT_ECONOMICS_SOURCES: tuple[Path, ...] = ( Path(__file__).resolve(), # registry changes must stale the stamp IMPORT_ECONOMICS_ENTRYPOINT, *_economy_import_modules(), + *_corporation_pages(), GENERATE_BRANDS_RS, GENERATE_BRANDS_NAMES_RS, GENERATE_BRANDS_SURNAMES_RS, @@ -132,6 +174,8 @@ IMPORT_ECONOMICS_SOURCES: tuple[Path, ...] = ( REPO_ROOT / "tooling" / "schema_version.py", SPECIALIZATION_VOCAB_TOML, SYSTEM_SPECIALIZATION_TOML, + CORP_HQ_PLACEMENT_TOML, + SETTLEMENT_NAME_LOCKED_TOML, ARCHITECTURE_TRAIT_CATALOG_TOML, ARCHITECTURE_TRAIT_BIAS_TOML, OBJECT_TAG_VOCABULARY_TOML, diff --git a/tooling/populate-corporations.sh b/tooling/populate-corporations.sh deleted file mode 100755 index 17abd0ff3..000000000 --- a/tooling/populate-corporations.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# Populate corporations table from wiki data + Cygni B combines + DSMC -set -euo pipefail -DB="$(cd "$(dirname "$0")/.." && pwd)/server/data/systems.db" - -sqlite3 "$DB" <<'SQL' --- From wiki/corporations/ -INSERT OR IGNORE INTO corporations VALUES ('gate-corporation', 'Gate Corporation', 'corporation', 'reach-wide', 'GJ 251', NULL, 'Span gate infrastructure', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('mvg', 'Mastroianni Vehicle Group', 'corporation', 'reach-wide', 'GJ 764', NULL, 'Transit vehicles, trains, haulers', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('prometheus-labs', 'Prometheus Labs', 'corporation', 'reach-wide', 'GJ 702B', NULL, 'Longevity hardware, neural augmentation', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('adams-ford', 'Adams & Ford Publishing', 'corporation', 'reach-wide', 'GJ 280A', NULL, 'The Drifters Guide to the Reach', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('mercado-travessia', 'Mercado Travessia', 'corporation', 'sector', 'GJ 1156', NULL, 'Grocery and household goods', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('stalownia-kowalski', 'Stalownia Kowalski', 'corporation', 'reach-wide', 'GJ 896A', NULL, 'Heavy mining and construction equipment', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('ferreira-monteiro', 'Ferreira Monteiro', 'corporation', 'reach-wide', 'GJ 884', NULL, 'Trade arbitration and commercial law', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('calloway-distillery', 'Calloway Distillery', 'corporation', 'sector', 'GJ 3325', NULL, 'Single malt whisky', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('thrds', 'thrds', 'corporation', 'sector', 'GJ 475', NULL, 'Cold-weather technical clothing', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('nordmark-skog', 'Nordmark Skog', 'corporation', 'sector', 'GJ 534', NULL, 'Timber and structural panels', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('talbrau', 'Talbräu', 'corporation', 'local', 'GJ 798', NULL, 'Franconian farmhouse lager', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('vins-de-grand-vide', 'Vins de Grand Vide', 'corporation', 'sector', 'GJ 395', NULL, 'Negociant wine cooperative', NULL, NULL, NULL); - --- DSMC from cultural-migration-pressure.md -INSERT OR IGNORE INTO corporations VALUES ('dsmc', 'Dorfhausen Sydney Mining Corporation', 'corporation', 'reach-wide', 'GJ 3522', NULL, 'Heavy extraction, mining infrastructure', NULL, NULL, NULL); - --- Cygni B combines (7 majors) -INSERT OR IGNORE INTO corporations VALUES ('vethara', 'Vethara Combine', 'combine', 'reach-wide', 'GJ 820B', NULL, 'Primary hull fabrication, capital-class freight haulers', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('kossler-drun', 'Kossler-Drun', 'combine', 'reach-wide', 'GJ 820B', NULL, 'Station module construction, habitat segments, docking architecture', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('amberan', 'Amberan Works', 'combine', 'reach-wide', 'GJ 820B', NULL, 'Gate-adjacent platform fabrication', NULL, 'Deepest involvement in Institute design-rights dispute', NULL); -INSERT OR IGNORE INTO corporations VALUES ('talavec', 'Talavec & Sons', 'combine', 'reach-wide', 'GJ 820B', NULL, 'Drive systems and propulsion assemblies', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('orren-skel', 'Orren-Skel', 'combine', 'reach-wide', 'GJ 820B', NULL, 'Precision components and sensor integration', NULL, 'The combine everyone subcontracts to', NULL); -INSERT OR IGNORE INTO corporations VALUES ('marhoud', 'Marhoud Fabrication', 'combine', 'reach-wide', 'GJ 820B', NULL, 'Freight hauler production lines, volume manufacturing', NULL, NULL, NULL); -INSERT OR IGNORE INTO corporations VALUES ('drevast', 'Drevast Extraction & Processing', 'combine', 'system', 'GJ 820B', NULL, 'In-system ore extraction and feedstock processing', NULL, 'Controls feedstock supply the other six need', NULL); -SQL - -echo "Populated $(sqlite3 "$DB" 'SELECT COUNT(*) FROM corporations') corporations" diff --git a/wiki/corporations/adams-ford-publishing.md b/wiki/corporations/adams-ford-publishing.md index d17dda11f..6863521c1 100644 --- a/wiki/corporations/adams-ford-publishing.md +++ b/wiki/corporations/adams-ford-publishing.md @@ -9,6 +9,7 @@ updated: 2026-03-14 scope: reach-wide faction_type: economic headquarters: Parallax (GJ 280A) +corp_specialization: media_center tags: [media, publishing, guide, humor, content] decision_refs: [] cross_refs: [parallax, lattice-commission, guardians-of-autonomy, ashwick-broadcasting] diff --git a/wiki/corporations/alcazar-hospitality.md b/wiki/corporations/alcazar-hospitality.md index 0bda65965..e9ba26a03 100644 --- a/wiki/corporations/alcazar-hospitality.md +++ b/wiki/corporations/alcazar-hospitality.md @@ -9,6 +9,7 @@ updated: 2026-04-21 scope: regional faction_type: economic headquarters: Altair (GJ 768) +corp_specialization: hospitality_hub tags: [service_premium, south_reach, tractus] decision_refs: [D-189] cross_refs: [the-registry] diff --git a/wiki/corporations/alcyone-tech.md b/wiki/corporations/alcyone-tech.md index 3fc17f4e3..f8166187f 100644 --- a/wiki/corporations/alcyone-tech.md +++ b/wiki/corporations/alcyone-tech.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: precision_tech tags: [tech_premium, core, tractus, electronics] decision_refs: [D-189] cross_refs: [groombridge-settlement-house, the-registry, meridian-risk] diff --git a/wiki/corporations/aldgate-trust.md b/wiki/corporations/aldgate-trust.md index 7edcd9194..b1e9cceb6 100644 --- a/wiki/corporations/aldgate-trust.md +++ b/wiki/corporations/aldgate-trust.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Crown's Hollow (GJ 661A) +corp_specialization: professional_services tags: [service_premium, north_reach, tractus] decision_refs: [D-189] cross_refs: [highland-cooperative, calloway-distillery, grampian-systems] diff --git a/wiki/corporations/alentejo-partners.md b/wiki/corporations/alentejo-partners.md index c32d33efb..86d8b24ca 100644 --- a/wiki/corporations/alentejo-partners.md +++ b/wiki/corporations/alentejo-partners.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Caparica (GJ 1002) +corp_specialization: terroir_agriculture tags: [terroir, south_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [algarve-botanics, ferreira-monteiro] diff --git a/wiki/corporations/algarve-botanics.md b/wiki/corporations/algarve-botanics.md index b73477a93..19c4659b8 100644 --- a/wiki/corporations/algarve-botanics.md +++ b/wiki/corporations/algarve-botanics.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Inhambane (GJ 54) +corp_specialization: terroir_spirits tags: [terroir, south_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [salud-alliance] diff --git a/wiki/corporations/arbour-aggregates.md b/wiki/corporations/arbour-aggregates.md index 5165f1fb5..8876c7a0e 100644 --- a/wiki/corporations/arbour-aggregates.md +++ b/wiki/corporations/arbour-aggregates.md @@ -9,6 +9,7 @@ updated: 2026-05-02 scope: GJ 338B local; core zone construction supply faction_type: economic headquarters: Arbour (GJ 338B) +corp_specialization: ore_extraction tags: [stone, timber, extraction, tractus] decision_refs: [D-175] cross_refs: [earth-standard-group] diff --git a/wiki/corporations/arclamp.md b/wiki/corporations/arclamp.md index ca18179b7..47f10d804 100644 --- a/wiki/corporations/arclamp.md +++ b/wiki/corporations/arclamp.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: general_industrial tags: [design_heritage, core, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [groombridge-settlement-house, alcyone-tech, comptoir-lendel] diff --git a/wiki/corporations/ashwick-broadcasting.md b/wiki/corporations/ashwick-broadcasting.md index d608ebf1a..858519b32 100644 --- a/wiki/corporations/ashwick-broadcasting.md +++ b/wiki/corporations/ashwick-broadcasting.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Ashfield (GJ 178) +corp_specialization: media_center tags: [cultural, north_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [adams-ford-publishing] diff --git a/wiki/corporations/atrium-culture.md b/wiki/corporations/atrium-culture.md index 6387db094..90bd6c6c6 100644 --- a/wiki/corporations/atrium-culture.md +++ b/wiki/corporations/atrium-culture.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: media_center tags: [cultural, core, tractus, entertainment] decision_refs: [D-189] cross_refs: [groombridge-settlement-house, the-registry] diff --git a/wiki/corporations/baektu-mining-alliance.md b/wiki/corporations/baektu-mining-alliance.md index e065aa696..933ade7cc 100644 --- a/wiki/corporations/baektu-mining-alliance.md +++ b/wiki/corporations/baektu-mining-alliance.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Isivikelo (GJ 1111) +corp_specialization: ore_extraction tags: [metallic_ore, rare_minerals, refined_metals, mining, south_reach, korean, assembly] decision_refs: [D-175] cross_refs: [stalownia-kowalski, umoja-freight, durban-engineering] diff --git a/wiki/corporations/baia-fundo-productions.md b/wiki/corporations/baia-fundo-productions.md index 159b11af3..02c571f54 100644 --- a/wiki/corporations/baia-fundo-productions.md +++ b/wiki/corporations/baia-fundo-productions.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Confluência (GJ 566B) +corp_specialization: media_center tags: [cultural, south_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [open-corridor-stream, quebrada-cultural, sertanejo-design] diff --git a/wiki/corporations/bavarian-craft.md b/wiki/corporations/bavarian-craft.md index fe88c5e58..3d64a10c9 100644 --- a/wiki/corporations/bavarian-craft.md +++ b/wiki/corporations/bavarian-craft.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Bergtor (GJ 505A) +corp_specialization: general_industrial tags: [heritage_craft, west_reach, mark, artisan] decision_refs: [D-189] cross_refs: [bergkraft-antriebswerke, steinbach-furniture, gronnmark-consumer] diff --git a/wiki/corporations/bergkraft-antriebswerke.md b/wiki/corporations/bergkraft-antriebswerke.md index 56dd54db7..b3cd03c27 100644 --- a/wiki/corporations/bergkraft-antriebswerke.md +++ b/wiki/corporations/bergkraft-antriebswerke.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Bergtor (GJ 505A) +corp_specialization: precision_tech tags: [drive_cores, tech_premium, west_reach, mark, electronics] decision_refs: [D-175, D-189] cross_refs: [bavarian-craft, rheintal-systems, durban-engineering] diff --git a/wiki/corporations/bifrost-marmor.md b/wiki/corporations/bifrost-marmor.md index 0140eb77b..eab0508d1 100644 --- a/wiki/corporations/bifrost-marmor.md +++ b/wiki/corporations/bifrost-marmor.md @@ -9,6 +9,7 @@ updated: 2026-03-30 scope: west_reach outer corridor; inner-corridor premium market faction_type: economic headquarters: Nyrheim (GJ 3737) +corp_specialization: marble_monopoly tags: [stone, quarrying, west_reach, mark] decision_refs: [D-175] cross_refs: [gate-corporation, GJ-3737] diff --git a/wiki/corporations/brindlewood-honey.md b/wiki/corporations/brindlewood-honey.md index 8a4d29b18..9f90bd77f 100644 --- a/wiki/corporations/brindlewood-honey.md +++ b/wiki/corporations/brindlewood-honey.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Ashfield (GJ 178) +corp_specialization: terroir_agriculture tags: [terroir, north_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [calloway-distillery, highland-cooperative, comptoir-lendel] diff --git a/wiki/corporations/byeolbit-entertainment.md b/wiki/corporations/byeolbit-entertainment.md index 82197539e..1b9589d02 100644 --- a/wiki/corporations/byeolbit-entertainment.md +++ b/wiki/corporations/byeolbit-entertainment.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Seongho (GJ 660A) +corp_specialization: media_center tags: [cultural, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [haesungdan-records] diff --git a/wiki/corporations/calloway-distillery.md b/wiki/corporations/calloway-distillery.md index 0769512f8..7a72d43f3 100644 --- a/wiki/corporations/calloway-distillery.md +++ b/wiki/corporations/calloway-distillery.md @@ -9,6 +9,7 @@ updated: 2026-03-16 scope: north_reach primary; inward corridor premium market secondary faction_type: economic headquarters: Calloway Reach (GJ 3325) +corp_specialization: terroir_spirits tags: [] decision_refs: [] cross_refs: [thrds, mercado-travessia] diff --git a/wiki/corporations/calloway-workshop.md b/wiki/corporations/calloway-workshop.md index a51e3d1bf..9a2e41e6b 100644 --- a/wiki/corporations/calloway-workshop.md +++ b/wiki/corporations/calloway-workshop.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Calloway Reach (GJ 3325) +corp_specialization: general_industrial tags: [design_heritage, north_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [calloway-distillery] diff --git a/wiki/corporations/calluna-wellness.md b/wiki/corporations/calluna-wellness.md index 6e71f0049..c2322347d 100644 --- a/wiki/corporations/calluna-wellness.md +++ b/wiki/corporations/calluna-wellness.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Cairn (GJ 845) +corp_specialization: hospitality_hub tags: [service_premium, north_reach, tractus] decision_refs: [D-189] cross_refs: [somatic-futures, prometheus-labs] diff --git a/wiki/corporations/cape-modern.md b/wiki/corporations/cape-modern.md index 2f9682fe7..675c300ce 100644 --- a/wiki/corporations/cape-modern.md +++ b/wiki/corporations/cape-modern.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Kaapse Baai (GJ 213) +corp_specialization: general_industrial tags: [design_heritage, south_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [sertanejo-design, quebrada-cultural, veldfontein-botanical] diff --git a/wiki/corporations/commonstock-foods.md b/wiki/corporations/commonstock-foods.md index b18efd0f4..d20e56bdf 100644 --- a/wiki/corporations/commonstock-foods.md +++ b/wiki/corporations/commonstock-foods.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Sirius (GJ 244A) +corp_specialization: general_industrial tags: [commodity_branded, core, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [mercado-travessia, westfield-common, iberian-standard-foods] diff --git a/wiki/corporations/compact-financial-exchange.md b/wiki/corporations/compact-financial-exchange.md index f20679eeb..cf630b44a 100644 --- a/wiki/corporations/compact-financial-exchange.md +++ b/wiki/corporations/compact-financial-exchange.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Rand (GJ 601A) +corp_specialization: financial_hub tags: [financial_services, clearing, mark, compact, west_reach] decision_refs: [D-171, D-172, D-175] cross_refs: [] diff --git a/wiki/corporations/comptoir-lendel.md b/wiki/corporations/comptoir-lendel.md index a037ec476..2605c9b61 100644 --- a/wiki/corporations/comptoir-lendel.md +++ b/wiki/corporations/comptoir-lendel.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: trade_distribution tags: [terroir, core, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [alcyone-tech, arclamp, calloway-distillery, brindlewood-honey] diff --git a/wiki/corporations/coriolis-exchange.md b/wiki/corporations/coriolis-exchange.md index e9cd1b0b4..c278d7827 100644 --- a/wiki/corporations/coriolis-exchange.md +++ b/wiki/corporations/coriolis-exchange.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: financial_hub tags: [service_premium, core, tractus] decision_refs: [D-189] cross_refs: [groombridge-settlement-house, alcyone-tech, meridian-risk, nordhavn-financial] diff --git a/wiki/corporations/cygni-combines.md b/wiki/corporations/cygni-combines.md index 5850d2936..1ee35b4ab 100644 --- a/wiki/corporations/cygni-combines.md +++ b/wiki/corporations/cygni-combines.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Cygni B (GJ 820B) +corp_specialization: general_industrial tags: [electronics, advanced_alloys, manufacturing, assembly, tractus, core] decision_refs: [D-175] cross_refs: [luanda-fabrication] diff --git a/wiki/corporations/dalbit-systems.md b/wiki/corporations/dalbit-systems.md index bac0ff1ff..1c31bea98 100644 --- a/wiki/corporations/dalbit-systems.md +++ b/wiki/corporations/dalbit-systems.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Seongho (GJ 660A) +corp_specialization: precision_tech tags: [tech_premium, east_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [higashiyama-vehicle, hangang-studio, kumho-navigation] diff --git a/wiki/corporations/destilaria-confluencia.md b/wiki/corporations/destilaria-confluencia.md index 5f74a63a3..0f7c32ae6 100644 --- a/wiki/corporations/destilaria-confluencia.md +++ b/wiki/corporations/destilaria-confluencia.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Confluência (GJ 566B) +corp_specialization: terroir_spirits tags: [terroir, south_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [matahari-consumer-goods] diff --git a/wiki/corporations/durban-engineering.md b/wiki/corporations/durban-engineering.md index e0b190693..3cadbea49 100644 --- a/wiki/corporations/durban-engineering.md +++ b/wiki/corporations/durban-engineering.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: south_reach primary; inner corridor extraction operations secondary faction_type: economic headquarters: Isibaya (GJ 905) +corp_specialization: precision_tech tags: [tech_premium, south_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [stalownia-kowalski, baektu-mining-alliance, luanda-fabrication] diff --git a/wiki/corporations/earth-standard-group.md b/wiki/corporations/earth-standard-group.md index 9abec0864..d4391df18 100644 --- a/wiki/corporations/earth-standard-group.md +++ b/wiki/corporations/earth-standard-group.md @@ -9,6 +9,7 @@ updated: 2026-04-21 scope: reach-wide faction_type: economic headquarters: Earth (GJ 0) +corp_specialization: general_industrial tags: [stone, advanced_alloys, industrial, sol_system] decision_refs: [D-175] cross_refs: [gate-corporation, arbour-aggregates] diff --git a/wiki/corporations/eastbay-trading.md b/wiki/corporations/eastbay-trading.md index 452001d34..55164dd97 100644 --- a/wiki/corporations/eastbay-trading.md +++ b/wiki/corporations/eastbay-trading.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Bridgepoint (GJ 53A) +corp_specialization: trade_distribution tags: [commodity_branded, east_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [nexus-way-express, dalbit-systems, commonstock-foods] diff --git a/wiki/corporations/endymion-instruments.md b/wiki/corporations/endymion-instruments.md index 2bcfd19f6..def4de343 100644 --- a/wiki/corporations/endymion-instruments.md +++ b/wiki/corporations/endymion-instruments.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: The Narrows (GJ 875) +corp_specialization: precision_tech tags: [tech_premium, deep_frontier, tractus, electronics] decision_refs: [D-189] cross_refs: [rare-vein-survey, dalbit-systems, grampian-systems] diff --git a/wiki/corporations/erzgebirge-instruments.md b/wiki/corporations/erzgebirge-instruments.md index 150188e92..3112beaa4 100644 --- a/wiki/corporations/erzgebirge-instruments.md +++ b/wiki/corporations/erzgebirge-instruments.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: Compact zone primary; west_reach manufacturing sector secondary faction_type: economic headquarters: Eisentor (GJ 3454) +corp_specialization: precision_tech tags: [tech_premium, west_reach, mark, electronics] decision_refs: [D-189] cross_refs: [westphalia-heavy-works, bergkraft-antriebswerke, compact-financial-exchange] diff --git a/wiki/corporations/ferreira-monteiro.md b/wiki/corporations/ferreira-monteiro.md index 7f06308a5..bc7fcced1 100644 --- a/wiki/corporations/ferreira-monteiro.md +++ b/wiki/corporations/ferreira-monteiro.md @@ -9,6 +9,7 @@ updated: 2026-03-15 scope: south_reach primary, reach-wide reputation faction_type: economic headquarters: Matamba (GJ 884) +corp_specialization: legal_archive tags: [] decision_refs: [] cross_refs: [luanda-fabrication] diff --git a/wiki/corporations/fieldborn-grain.md b/wiki/corporations/fieldborn-grain.md index 1e07a8c48..87348451a 100644 --- a/wiki/corporations/fieldborn-grain.md +++ b/wiki/corporations/fieldborn-grain.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Calloway Reach (GJ 3325) +corp_specialization: terroir_agriculture tags: [commodity_branded, north_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [calloway-distillery, highland-cooperative, grampian-systems] diff --git a/wiki/corporations/fjeldtone-music.md b/wiki/corporations/fjeldtone-music.md index d61e3608b..61fc33e9e 100644 --- a/wiki/corporations/fjeldtone-music.md +++ b/wiki/corporations/fjeldtone-music.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Halvøy (GJ 624) +corp_specialization: media_center tags: [cultural, west_reach, mark, entertainment] decision_refs: [D-189] cross_refs: [graben-studio, open-corridor-stream, sungshin-music] diff --git a/wiki/corporations/gate-corporation.md b/wiki/corporations/gate-corporation.md index f7791db1a..b4b2b6755 100644 --- a/wiki/corporations/gate-corporation.md +++ b/wiki/corporations/gate-corporation.md @@ -9,6 +9,7 @@ updated: 2026-03-14 scope: reach-wide faction_type: economic headquarters: Renaissance (GJ 251) +corp_specialization: gate_fabrication tags: [gate_components, gate_infrastructure, reach_wide, monopolist] decision_refs: [D-095, D-175] cross_refs: [kaur-observatory-equipment] diff --git a/wiki/corporations/graben-studio.md b/wiki/corporations/graben-studio.md index e1a8cd5cf..5c80b8fe6 100644 --- a/wiki/corporations/graben-studio.md +++ b/wiki/corporations/graben-studio.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Freiholt (GJ 453) +corp_specialization: media_center tags: [cultural, west_reach, mark, entertainment] decision_refs: [D-189] cross_refs: [fjeldtone-music, maas-rijn-entertainment, wolkenbruch-cinema] diff --git a/wiki/corporations/grampian-systems.md b/wiki/corporations/grampian-systems.md index 82177800e..63b5c1ff6 100644 --- a/wiki/corporations/grampian-systems.md +++ b/wiki/corporations/grampian-systems.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Calloway Reach (GJ 3325) +corp_specialization: precision_tech tags: [tech_premium, north_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [fieldborn-grain, calloway-distillery, aldgate-trust] diff --git a/wiki/corporations/gridsupply-electronics.md b/wiki/corporations/gridsupply-electronics.md index 623a558fd..4c39300f7 100644 --- a/wiki/corporations/gridsupply-electronics.md +++ b/wiki/corporations/gridsupply-electronics.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: general_industrial tags: [commodity_branded, core, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [alcyone-tech, arclamp, commonstock-foods] diff --git a/wiki/corporations/gronnmark-consumer.md b/wiki/corporations/gronnmark-consumer.md index 4c946f751..a1f4a157b 100644 --- a/wiki/corporations/gronnmark-consumer.md +++ b/wiki/corporations/gronnmark-consumer.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Nyrheim (GJ 3737) +corp_specialization: general_industrial tags: [commodity_branded, west_reach, mark, consumer_goods] decision_refs: [D-189] cross_refs: [bavarian-craft, bergkraft-antriebswerke, lowlands-consumer] diff --git a/wiki/corporations/groombridge-settlement-house.md b/wiki/corporations/groombridge-settlement-house.md index 0d42cd72d..753fc2284 100644 --- a/wiki/corporations/groombridge-settlement-house.md +++ b/wiki/corporations/groombridge-settlement-house.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Lendel (GJ 380) +corp_specialization: financial_hub tags: [financial_services, clearing, tractus, assembly, groombridge] decision_refs: [D-171, D-175, D-182] cross_refs: [] diff --git a/wiki/corporations/groot-karoo-cellars.md b/wiki/corporations/groot-karoo-cellars.md index 8b464ab7f..47bb9f5b0 100644 --- a/wiki/corporations/groot-karoo-cellars.md +++ b/wiki/corporations/groot-karoo-cellars.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Karoo (GJ 222A) +corp_specialization: terroir_spirits tags: [terroir, south_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [kaapse-baai-wines] diff --git a/wiki/corporations/haesungdan-records.md b/wiki/corporations/haesungdan-records.md index 5e81a270d..d518a2e26 100644 --- a/wiki/corporations/haesungdan-records.md +++ b/wiki/corporations/haesungdan-records.md @@ -9,6 +9,7 @@ updated: 2026-04-21 scope: regional faction_type: economic headquarters: Miryang (GJ 754) +corp_specialization: media_center tags: [cultural, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [] diff --git a/wiki/corporations/hakone-hospitality.md b/wiki/corporations/hakone-hospitality.md index 02ea8627c..2b344bf78 100644 --- a/wiki/corporations/hakone-hospitality.md +++ b/wiki/corporations/hakone-hospitality.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Matsue (GJ 687) +corp_specialization: hospitality_hub tags: [service_premium, east_reach, tractus] decision_refs: [D-189] cross_refs: [prometheus-labs, lattice-commission] diff --git a/wiki/corporations/hana-creative.md b/wiki/corporations/hana-creative.md index b4207b2ad..f7c2fdf86 100644 --- a/wiki/corporations/hana-creative.md +++ b/wiki/corporations/hana-creative.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Jinghu (GJ 445) +corp_specialization: media_center tags: [cultural, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [hangang-studio, ondori-interactive, byeolbit-entertainment] diff --git a/wiki/corporations/hangang-studio.md b/wiki/corporations/hangang-studio.md index 930bc8f16..4ed632c85 100644 --- a/wiki/corporations/hangang-studio.md +++ b/wiki/corporations/hangang-studio.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Songjiang (GJ 625) +corp_specialization: media_center tags: [platform_catalogue, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [hana-creative, open-corridor-stream, byeolbit-entertainment] diff --git a/wiki/corporations/hangil-celadon.md b/wiki/corporations/hangil-celadon.md index 0588bf2d6..8e8460405 100644 --- a/wiki/corporations/hangil-celadon.md +++ b/wiki/corporations/hangil-celadon.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Miryang (GJ 754) +corp_specialization: general_industrial tags: [heritage_craft, east_reach, tractus, artisan] decision_refs: [D-189] cross_refs: [hangil-celadon, hana-creative, iznik-atelier] diff --git a/wiki/corporations/hanyang-precision.md b/wiki/corporations/hanyang-precision.md index 018451c73..73902c9cd 100644 --- a/wiki/corporations/hanyang-precision.md +++ b/wiki/corporations/hanyang-precision.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Changwon (GJ 860B) +corp_specialization: precision_tech tags: [electronics, lattice_substrate, precision_instruments, east_reach, korean, assembly] decision_refs: [D-175] cross_refs: [kaur-observatory-equipment] diff --git a/wiki/corporations/hartsteen-legal.md b/wiki/corporations/hartsteen-legal.md index 2dfcb0954..7b6c6d2b3 100644 --- a/wiki/corporations/hartsteen-legal.md +++ b/wiki/corporations/hartsteen-legal.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Confluent (GJ 395) +corp_specialization: legal_archive tags: [service_premium, west_reach, mark] decision_refs: [D-189] cross_refs: [bavarian-craft, bergkraft-antriebswerke, nordhavn-financial] diff --git a/wiki/corporations/higashiyama-vehicle.md b/wiki/corporations/higashiyama-vehicle.md index b5e77a25d..68e1107ef 100644 --- a/wiki/corporations/higashiyama-vehicle.md +++ b/wiki/corporations/higashiyama-vehicle.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Kurashiki (GJ 205) +corp_specialization: vehicle_production tags: [tech_premium, east_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [dalbit-systems, higashiyama-vehicle, kumho-navigation] diff --git a/wiki/corporations/highland-cooperative.md b/wiki/corporations/highland-cooperative.md index 56f10c604..6be464b85 100644 --- a/wiki/corporations/highland-cooperative.md +++ b/wiki/corporations/highland-cooperative.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Crown's Hollow (GJ 661A) +corp_specialization: terroir_agriculture tags: [terroir, north_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [aldgate-trust, calloway-distillery, brindlewood-honey] diff --git a/wiki/corporations/iberian-standard-foods.md b/wiki/corporations/iberian-standard-foods.md index 795e31224..75d0ca237 100644 --- a/wiki/corporations/iberian-standard-foods.md +++ b/wiki/corporations/iberian-standard-foods.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Caparica (GJ 1002) +corp_specialization: general_industrial tags: [commodity_branded, south_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [alentejo-partners, destilaria-confluencia, mercado-travessia] diff --git a/wiki/corporations/incheon-salt.md b/wiki/corporations/incheon-salt.md index c44d2cbb4..2d82995f5 100644 --- a/wiki/corporations/incheon-salt.md +++ b/wiki/corporations/incheon-salt.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Changwon (GJ 860B) +corp_specialization: terroir_agriculture tags: [terroir, east_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [koryo-standard, kumho-navigation] diff --git a/wiki/corporations/ironbark-films.md b/wiki/corporations/ironbark-films.md index deaa0f384..57b4dbfcd 100644 --- a/wiki/corporations/ironbark-films.md +++ b/wiki/corporations/ironbark-films.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Fremantle (GJ 273) +corp_specialization: media_center tags: [cultural, north_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [ashwick-broadcasting, calloway-distillery] diff --git a/wiki/corporations/iznik-atelier.md b/wiki/corporations/iznik-atelier.md index e3b39f518..dddb593ff 100644 --- a/wiki/corporations/iznik-atelier.md +++ b/wiki/corporations/iznik-atelier.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: general_industrial tags: [heritage_craft, core, tractus, artisan] decision_refs: [D-189] cross_refs: [alcyone-tech, the-registry, groombridge-settlement-house] diff --git a/wiki/corporations/jeju-lattice.md b/wiki/corporations/jeju-lattice.md index a24f3ee62..1c1f1b079 100644 --- a/wiki/corporations/jeju-lattice.md +++ b/wiki/corporations/jeju-lattice.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Yeongwol (GJ 268) +corp_specialization: precision_tech tags: [lattice_grade_material, lattice_substrate, tech_premium, east_reach, tractus, electronics] decision_refs: [D-175, D-189] cross_refs: [kumho-navigation, namsan-collective, kyoei-design] diff --git a/wiki/corporations/jeonnam-old-table.md b/wiki/corporations/jeonnam-old-table.md index 224488c49..f9534632c 100644 --- a/wiki/corporations/jeonnam-old-table.md +++ b/wiki/corporations/jeonnam-old-table.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Changwon (GJ 860B) +corp_specialization: terroir_agriculture tags: [organic_compounds, processed_food, east_reach, korean, fermentation, cooperative] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/kaapse-baai-wines.md b/wiki/corporations/kaapse-baai-wines.md index 9e100ebbd..b37b51c31 100644 --- a/wiki/corporations/kaapse-baai-wines.md +++ b/wiki/corporations/kaapse-baai-wines.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Kaapse Baai (GJ 213) +corp_specialization: terroir_spirits tags: [terroir, south_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [groot-karoo-cellars, meridional-banking] diff --git a/wiki/corporations/kalahari-leatherworks.md b/wiki/corporations/kalahari-leatherworks.md index 17174392c..8fa42f2c0 100644 --- a/wiki/corporations/kalahari-leatherworks.md +++ b/wiki/corporations/kalahari-leatherworks.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Karoo (GJ 222A) +corp_specialization: general_industrial tags: [heritage_craft, south_reach, tractus, artisan] decision_refs: [D-189] cross_refs: [groot-karoo-cellars, iberian-standard-foods, alentejo-partners] diff --git a/wiki/corporations/kallast-agrosyndic.md b/wiki/corporations/kallast-agrosyndic.md index e47e03fd9..5077b574f 100644 --- a/wiki/corporations/kallast-agrosyndic.md +++ b/wiki/corporations/kallast-agrosyndic.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Kallast (GJ 144) +corp_specialization: breadbasket tags: [agricultural_produce, processed_food, tractus, syndic, ran, monopolist] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/kaur-observatory-equipment.md b/wiki/corporations/kaur-observatory-equipment.md index aacd8d177..17af4e2f8 100644 --- a/wiki/corporations/kaur-observatory-equipment.md +++ b/wiki/corporations/kaur-observatory-equipment.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: east_reach primary; reach-wide scientific and navigation instrument contracts secondary faction_type: economic headquarters: Kaur's Observatory (GJ 432A) +corp_specialization: precision_tech tags: [tech_premium, east_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [hanyang-precision, dalbit-systems] diff --git a/wiki/corporations/kellervolk.md b/wiki/corporations/kellervolk.md index da0a728b4..0356b9a16 100644 --- a/wiki/corporations/kellervolk.md +++ b/wiki/corporations/kellervolk.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Nyrheim (GJ 3737) +corp_specialization: media_center tags: [cultural, west_reach, mark, entertainment] decision_refs: [D-189] cross_refs: [nordhavn-financial, norrland-woodcraft, maas-rijn-entertainment] diff --git a/wiki/corporations/kinlochewe-instruments.md b/wiki/corporations/kinlochewe-instruments.md index f8455f753..2d73480fb 100644 --- a/wiki/corporations/kinlochewe-instruments.md +++ b/wiki/corporations/kinlochewe-instruments.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Braemar (GJ 475) +corp_specialization: precision_tech tags: [tech_premium, north_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [calloway-distillery, ironbark-films, erzgebirge-instruments] diff --git a/wiki/corporations/koryo-standard.md b/wiki/corporations/koryo-standard.md index 7e3ea0251..44b96a6af 100644 --- a/wiki/corporations/koryo-standard.md +++ b/wiki/corporations/koryo-standard.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Jeonnam (GJ 3728B) +corp_specialization: general_industrial tags: [commodity_branded, east_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [incheon-salt, kyoei-design, jeju-lattice] diff --git a/wiki/corporations/kovalev-freight.md b/wiki/corporations/kovalev-freight.md index 096826d18..7f40f857f 100644 --- a/wiki/corporations/kovalev-freight.md +++ b/wiki/corporations/kovalev-freight.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Prometheus (GJ 702B) +corp_specialization: trade_distribution tags: [agricultural_produce, processed_food, freight, tractus, assembly, russian_heritage] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/kumho-navigation.md b/wiki/corporations/kumho-navigation.md index 8ba9519ca..52b61520c 100644 --- a/wiki/corporations/kumho-navigation.md +++ b/wiki/corporations/kumho-navigation.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Hwaseong (GJ 1125) +corp_specialization: precision_tech tags: [tech_premium, east_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [jeju-lattice, koryo-standard, the-gate-corporation] diff --git a/wiki/corporations/kyoei-design.md b/wiki/corporations/kyoei-design.md index 6cb02641b..a8b9974a5 100644 --- a/wiki/corporations/kyoei-design.md +++ b/wiki/corporations/kyoei-design.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Kurashiki (GJ 205) +corp_specialization: general_industrial tags: [design_heritage, east_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [kyoto-valley-estates, mirai-entertainment, namsan-collective] diff --git a/wiki/corporations/kyoto-valley-estates.md b/wiki/corporations/kyoto-valley-estates.md index 04ff9f7ac..bf2e330df 100644 --- a/wiki/corporations/kyoto-valley-estates.md +++ b/wiki/corporations/kyoto-valley-estates.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Kurashiki (GJ 205) +corp_specialization: terroir_spirits tags: [terroir, east_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [kyoei-design, mirai-entertainment, calloway-distillery] diff --git a/wiki/corporations/lagrange-fuel-systems.md b/wiki/corporations/lagrange-fuel-systems.md index 470c3b4a2..1f919fb8d 100644 --- a/wiki/corporations/lagrange-fuel-systems.md +++ b/wiki/corporations/lagrange-fuel-systems.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Lagrange Station (GJ 445) +corp_specialization: fuel_production tags: [fusion_fuel, water, ice_harvesting, frontier, independent, monopolist] decision_refs: [D-175, D-186, D-187] cross_refs: [] diff --git a/wiki/corporations/leerfeld-records.md b/wiki/corporations/leerfeld-records.md index 73fb8b56f..1983e910a 100644 --- a/wiki/corporations/leerfeld-records.md +++ b/wiki/corporations/leerfeld-records.md @@ -9,6 +9,7 @@ updated: 2026-04-21 scope: regional faction_type: economic headquarters: Confluent (GJ 395) +corp_specialization: media_center tags: [cultural, west_reach, mark, entertainment] decision_refs: [D-189] cross_refs: [] diff --git a/wiki/corporations/lowlands-consumer.md b/wiki/corporations/lowlands-consumer.md index f2c64c25f..958df1112 100644 --- a/wiki/corporations/lowlands-consumer.md +++ b/wiki/corporations/lowlands-consumer.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Dokkum (GJ 506) +corp_specialization: general_industrial tags: [commodity_branded, west_reach, mark, consumer_goods] decision_refs: [D-189] cross_refs: [maas-rijn-entertainment, nordhavn-financial, norrland-woodcraft] diff --git a/wiki/corporations/luanda-fabrication.md b/wiki/corporations/luanda-fabrication.md index 5e86d3cc0..582815519 100644 --- a/wiki/corporations/luanda-fabrication.md +++ b/wiki/corporations/luanda-fabrication.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: south_reach primary; specialist supply contracts to inner corridor secondary faction_type: economic headquarters: Matamba (GJ 884) +corp_specialization: precision_tech tags: [tech_premium, south_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [ferreira-monteiro, durban-engineering, cygni-combines] diff --git a/wiki/corporations/lusaka-advisory.md b/wiki/corporations/lusaka-advisory.md index 47dbe9c24..27e1af43d 100644 --- a/wiki/corporations/lusaka-advisory.md +++ b/wiki/corporations/lusaka-advisory.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Zenzele (GJ 1111) +corp_specialization: professional_services tags: [service_premium, south_reach, tractus] decision_refs: [D-189] cross_refs: [ferreira-monteiro, baektu-mining-alliance] diff --git a/wiki/corporations/lusitanian-glass.md b/wiki/corporations/lusitanian-glass.md index 67d6c33fd..5b356ade4 100644 --- a/wiki/corporations/lusitanian-glass.md +++ b/wiki/corporations/lusitanian-glass.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Caparica (GJ 1002) +corp_specialization: general_industrial tags: [heritage_craft, south_reach, tractus, artisan] decision_refs: [D-189] cross_refs: [iberian-standard-foods, alentejo-partners, destilaria-confluencia] diff --git a/wiki/corporations/maas-rijn-entertainment.md b/wiki/corporations/maas-rijn-entertainment.md index ad74df0a4..605ddb3ff 100644 --- a/wiki/corporations/maas-rijn-entertainment.md +++ b/wiki/corporations/maas-rijn-entertainment.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Dokkum (GJ 506) +corp_specialization: media_center tags: [cultural, west_reach, mark, entertainment] decision_refs: [D-189] cross_refs: [kellervolk, lowlands-consumer, ashwick-broadcasting] diff --git a/wiki/corporations/maison-cinq.md b/wiki/corporations/maison-cinq.md index d19634ad7..41dbbd72b 100644 --- a/wiki/corporations/maison-cinq.md +++ b/wiki/corporations/maison-cinq.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Sirius (GJ 244A) +corp_specialization: general_industrial tags: [design_heritage, core, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [the-registry] diff --git a/wiki/corporations/makuti-consumer.md b/wiki/corporations/makuti-consumer.md index 5344c8cf4..2fe13c718 100644 --- a/wiki/corporations/makuti-consumer.md +++ b/wiki/corporations/makuti-consumer.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Mwangaza (GJ 693) +corp_specialization: general_industrial tags: [commodity_branded, south_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [mwanga-media, iberian-standard-foods, kalahari-leatherworks] diff --git a/wiki/corporations/mastroianni-vehicle-group.md b/wiki/corporations/mastroianni-vehicle-group.md index 9694589b9..fdbc7dd83 100644 --- a/wiki/corporations/mastroianni-vehicle-group.md +++ b/wiki/corporations/mastroianni-vehicle-group.md @@ -9,6 +9,7 @@ updated: 2026-03-14 scope: reach-wide faction_type: economic headquarters: Nova Roma (GJ 764) +corp_specialization: vehicle_production tags: [] decision_refs: [] cross_refs: [trans-reach-rail] diff --git a/wiki/corporations/matahari-consumer-goods.md b/wiki/corporations/matahari-consumer-goods.md index 5b6c29d5a..966792287 100644 --- a/wiki/corporations/matahari-consumer-goods.md +++ b/wiki/corporations/matahari-consumer-goods.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Confluência (GJ 566B) +corp_specialization: general_industrial tags: [consumer_goods, textiles, tractus, assembly, malay_heritage, south_reach] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/matosinhos-fishing.md b/wiki/corporations/matosinhos-fishing.md index 9731f6a67..a179cfebd 100644 --- a/wiki/corporations/matosinhos-fishing.md +++ b/wiki/corporations/matosinhos-fishing.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Velha Guarda (GJ 4A) +corp_specialization: marine_farming tags: [commodity_branded, south_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [iberian-standard-foods, algarve-botanics, alentejo-partners] diff --git a/wiki/corporations/mercado-travessia.md b/wiki/corporations/mercado-travessia.md index 3177fa130..e7da25426 100644 --- a/wiki/corporations/mercado-travessia.md +++ b/wiki/corporations/mercado-travessia.md @@ -9,6 +9,7 @@ updated: 2026-03-15 scope: reach-wide faction_type: economic headquarters: Travessia (GJ 1156) +corp_specialization: trade_distribution tags: [] decision_refs: [] cross_refs: [] diff --git a/wiki/corporations/meridian-risk.md b/wiki/corporations/meridian-risk.md index d30419fd3..1ec24112f 100644 --- a/wiki/corporations/meridian-risk.md +++ b/wiki/corporations/meridian-risk.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Lendel (GJ 380) +corp_specialization: professional_services tags: [insurance, financial_services, tractus, assembly, groombridge] decision_refs: [D-171, D-175] cross_refs: [] diff --git a/wiki/corporations/meridional-banking.md b/wiki/corporations/meridional-banking.md index 2306d89bd..4b6ac6fc7 100644 --- a/wiki/corporations/meridional-banking.md +++ b/wiki/corporations/meridional-banking.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Meridian (GJ 674) +corp_specialization: financial_hub tags: [service_premium, core, tractus] decision_refs: [D-189] cross_refs: [groombridge-settlement-house, meridional-technical, meridian-risk, alcyone-tech] diff --git a/wiki/corporations/meridional-technical.md b/wiki/corporations/meridional-technical.md index 732906040..a1b4a48fb 100644 --- a/wiki/corporations/meridional-technical.md +++ b/wiki/corporations/meridional-technical.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Meridian (GJ 674) +corp_specialization: precision_tech tags: [tech_premium, core, tractus, electronics] decision_refs: [D-189] cross_refs: [meridional-banking, alcyone-tech, groombridge-settlement-house] diff --git a/wiki/corporations/mirai-entertainment.md b/wiki/corporations/mirai-entertainment.md index a8c094972..5248a0ed4 100644 --- a/wiki/corporations/mirai-entertainment.md +++ b/wiki/corporations/mirai-entertainment.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Kurashiki (GJ 205) +corp_specialization: media_center tags: [cultural, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [kyoei-design, kyoto-valley-estates, namsan-collective] diff --git a/wiki/corporations/moorfield-records.md b/wiki/corporations/moorfield-records.md index c8103eb3f..022d9b920 100644 --- a/wiki/corporations/moorfield-records.md +++ b/wiki/corporations/moorfield-records.md @@ -9,6 +9,7 @@ updated: 2026-04-21 scope: regional faction_type: economic headquarters: Braemar (GJ 475) +corp_specialization: media_center tags: [cultural, north_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [] diff --git a/wiki/corporations/mwanga-media.md b/wiki/corporations/mwanga-media.md index 5eb626b85..118f27c77 100644 --- a/wiki/corporations/mwanga-media.md +++ b/wiki/corporations/mwanga-media.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Mwangaza (GJ 693) +corp_specialization: media_center tags: [cultural, south_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [makuti-consumer, ironbark-films, mwangaza] diff --git a/wiki/corporations/namsan-collective.md b/wiki/corporations/namsan-collective.md index 820c44090..bd8a936b6 100644 --- a/wiki/corporations/namsan-collective.md +++ b/wiki/corporations/namsan-collective.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Yeongwol (GJ 268) +corp_specialization: media_center tags: [cultural, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [jeju-lattice, mirai-entertainment, kyoei-design] diff --git a/wiki/corporations/nexus-way-express.md b/wiki/corporations/nexus-way-express.md index e381fd3b9..68006a0f8 100644 --- a/wiki/corporations/nexus-way-express.md +++ b/wiki/corporations/nexus-way-express.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Sirius (GJ 244A) +corp_specialization: trade_distribution tags: [service_premium, core, tractus] decision_refs: [D-189] cross_refs: [the-gate-corporation, meridional-banking, groombridge-settlement-house] diff --git a/wiki/corporations/nordhavn-financial.md b/wiki/corporations/nordhavn-financial.md index b99ec34e2..dc6a6d49b 100644 --- a/wiki/corporations/nordhavn-financial.md +++ b/wiki/corporations/nordhavn-financial.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Nyrheim (GJ 3737) +corp_specialization: financial_hub tags: [service_premium, west_reach, mark] decision_refs: [D-189] cross_refs: [kellervolk, norrland-woodcraft, lowlands-consumer] diff --git a/wiki/corporations/nordmark-skog.md b/wiki/corporations/nordmark-skog.md index e52695a34..23213a430 100644 --- a/wiki/corporations/nordmark-skog.md +++ b/wiki/corporations/nordmark-skog.md @@ -9,6 +9,7 @@ updated: 2026-03-16 scope: west_reach outer corridor faction_type: economic headquarters: Stillvakt (GJ 534) +corp_specialization: general_industrial tags: [timber, forestry, west_reach, mark] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/nordschatten-records.md b/wiki/corporations/nordschatten-records.md index c58fcec22..aa771d186 100644 --- a/wiki/corporations/nordschatten-records.md +++ b/wiki/corporations/nordschatten-records.md @@ -9,6 +9,7 @@ updated: 2026-04-21 scope: regional faction_type: economic headquarters: Nyrheim (GJ 3737) +corp_specialization: media_center tags: [cultural, west_reach, mark, entertainment] decision_refs: [D-189] cross_refs: [] diff --git a/wiki/corporations/norrland-woodcraft.md b/wiki/corporations/norrland-woodcraft.md index 247c4a0b5..76f4f0dea 100644 --- a/wiki/corporations/norrland-woodcraft.md +++ b/wiki/corporations/norrland-woodcraft.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Nyrheim (GJ 3737) +corp_specialization: general_industrial tags: [timber, heritage_craft, west_reach, mark, artisan] decision_refs: [D-175, D-189] cross_refs: [kellervolk, nordhavn-financial, lowlands-consumer] diff --git a/wiki/corporations/ondori-interactive.md b/wiki/corporations/ondori-interactive.md index e85611e67..3fcec311d 100644 --- a/wiki/corporations/ondori-interactive.md +++ b/wiki/corporations/ondori-interactive.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Jeonnam (GJ 3728B) +corp_specialization: media_center tags: [cultural, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [sungshin-music, open-corridor-stream, resonance-premium] diff --git a/wiki/corporations/open-corridor-stream.md b/wiki/corporations/open-corridor-stream.md index d8f6671a4..95ab1b112 100644 --- a/wiki/corporations/open-corridor-stream.md +++ b/wiki/corporations/open-corridor-stream.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: media_center tags: [platform_catalogue, core, tractus, entertainment] decision_refs: [D-189] cross_refs: [ondori-interactive, reach-standard-media, sungshin-music, resonance-premium, reach-archive-standard, transit-library] diff --git a/wiki/corporations/orkney-ceramics.md b/wiki/corporations/orkney-ceramics.md index dd8b49f18..253e198f8 100644 --- a/wiki/corporations/orkney-ceramics.md +++ b/wiki/corporations/orkney-ceramics.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Cairn (GJ 845) +corp_specialization: general_industrial tags: [heritage_craft, north_reach, tractus, artisan] decision_refs: [D-189] cross_refs: [highland-cooperative, shetland-wool] diff --git a/wiki/corporations/pembrokeshire-smithy.md b/wiki/corporations/pembrokeshire-smithy.md index 397b217ab..17a353256 100644 --- a/wiki/corporations/pembrokeshire-smithy.md +++ b/wiki/corporations/pembrokeshire-smithy.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Pemberton (GJ 194B) +corp_specialization: general_industrial tags: [heritage_craft, north_reach, tractus, artisan] decision_refs: [D-189] cross_refs: [sunderland-precision, kinlochewe-instruments, sendai-forge] diff --git a/wiki/corporations/plainfield-agricultural.md b/wiki/corporations/plainfield-agricultural.md index b5b2c9e31..20977d242 100644 --- a/wiki/corporations/plainfield-agricultural.md +++ b/wiki/corporations/plainfield-agricultural.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Fremantle (GJ 273) +corp_specialization: breadbasket tags: [commodity_branded, north_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [fieldborn-grain, highland-cooperative, commonstock-foods] diff --git a/wiki/corporations/porto-alegre-technical.md b/wiki/corporations/porto-alegre-technical.md index 36b8b4478..2952400ed 100644 --- a/wiki/corporations/porto-alegre-technical.md +++ b/wiki/corporations/porto-alegre-technical.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Nowa Huta (GJ 896A) +corp_specialization: precision_tech tags: [tech_premium, south_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [endymion-instruments, somatic-futures] diff --git a/wiki/corporations/prometheus-labs.md b/wiki/corporations/prometheus-labs.md index 0d84c9e01..4eeb7f987 100644 --- a/wiki/corporations/prometheus-labs.md +++ b/wiki/corporations/prometheus-labs.md @@ -9,6 +9,7 @@ updated: 2026-03-14 scope: reach-wide faction_type: economic headquarters: Prometheus (GJ 702B) +corp_specialization: longevity_monopoly tags: [] decision_refs: [] cross_refs: [] diff --git a/wiki/corporations/quebrada-cultural.md b/wiki/corporations/quebrada-cultural.md index 102904098..4a59b3954 100644 --- a/wiki/corporations/quebrada-cultural.md +++ b/wiki/corporations/quebrada-cultural.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Matamba (GJ 884) +corp_specialization: media_center tags: [cultural, south_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [open-corridor-stream, baia-fundo-productions] diff --git a/wiki/corporations/rare-vein-survey.md b/wiki/corporations/rare-vein-survey.md index cba00b5de..76184a259 100644 --- a/wiki/corporations/rare-vein-survey.md +++ b/wiki/corporations/rare-vein-survey.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Parallax (GJ 280A) +corp_specialization: rare_mineral_extraction tags: [rare_minerals, lattice_grade_material, mining, assembly, specialist] decision_refs: [D-175, D-177] cross_refs: [] diff --git a/wiki/corporations/reach-archive-standard.md b/wiki/corporations/reach-archive-standard.md index ac56b3b59..07de4336a 100644 --- a/wiki/corporations/reach-archive-standard.md +++ b/wiki/corporations/reach-archive-standard.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Sirius (GJ 244A) +corp_specialization: legal_archive tags: [platform_catalogue, core, tractus, entertainment] decision_refs: [D-189] cross_refs: [open-corridor-stream, transit-library, resonance-premium, reach-standard-media] diff --git a/wiki/corporations/reach-standard-media.md b/wiki/corporations/reach-standard-media.md index 9c169ff44..047b7851a 100644 --- a/wiki/corporations/reach-standard-media.md +++ b/wiki/corporations/reach-standard-media.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Sirius (GJ 244A) +corp_specialization: media_center tags: [cultural, core, tractus, entertainment] decision_refs: [D-189] cross_refs: [reach-archive-standard, ashwick-broadcasting, open-corridor-stream, thorngate-media] diff --git a/wiki/corporations/reach-transport.md b/wiki/corporations/reach-transport.md index f63dea14a..da508c125 100644 --- a/wiki/corporations/reach-transport.md +++ b/wiki/corporations/reach-transport.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Sirius (GJ 244A) +corp_specialization: trade_distribution tags: [commodity_branded, core, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [nexus-way-express, westmark-distribution, gate-corporation] diff --git a/wiki/corporations/resonance-premium.md b/wiki/corporations/resonance-premium.md index bc585ce2a..6eff0ac80 100644 --- a/wiki/corporations/resonance-premium.md +++ b/wiki/corporations/resonance-premium.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Sirius (GJ 244A) +corp_specialization: media_center tags: [platform_catalogue, core, tractus, entertainment] decision_refs: [D-189] cross_refs: [open-corridor-stream, transit-library, reach-archive-standard, ondori-interactive] diff --git a/wiki/corporations/rheingold-distillers.md b/wiki/corporations/rheingold-distillers.md index 89390d0ee..179363d61 100644 --- a/wiki/corporations/rheingold-distillers.md +++ b/wiki/corporations/rheingold-distillers.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Rødvik (GJ 581) +corp_specialization: terroir_spirits tags: [terroir, west_reach, mark, scarcity_class_rare] decision_refs: [D-189] cross_refs: [schwarzwald-gin, svanevann-waters, mercado-travessia] diff --git a/wiki/corporations/rheintal-systems.md b/wiki/corporations/rheintal-systems.md index d8fe33f98..045326b0e 100644 --- a/wiki/corporations/rheintal-systems.md +++ b/wiki/corporations/rheintal-systems.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Neustadt (GJ 526) +corp_specialization: precision_tech tags: [tech_premium, west_reach, mark, electronics] decision_refs: [D-189] cross_refs: [bergkraft-antriebswerke, sunderland-precision, porto-alegre-technical] diff --git a/wiki/corporations/rijdbaar.md b/wiki/corporations/rijdbaar.md index 8ce318dd6..083ffed8b 100644 --- a/wiki/corporations/rijdbaar.md +++ b/wiki/corporations/rijdbaar.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Confluent (GJ 395) +corp_specialization: general_industrial tags: [design_heritage, west_reach, mark, consumer_goods] decision_refs: [D-189] cross_refs: [steinbach-furniture, vlaams-atelier, sertanejo-design] diff --git a/wiki/corporations/rush-mining.md b/wiki/corporations/rush-mining.md index 70853f713..9713268c1 100644 --- a/wiki/corporations/rush-mining.md +++ b/wiki/corporations/rush-mining.md @@ -9,6 +9,7 @@ updated: 2026-05-02 scope: GJ 725B local; corridor-wide secondary faction_type: economic headquarters: Rush (GJ 725B) +corp_specialization: company_mining tags: [metallic_ore, rare_minerals, lattice_grade_material, mining, frontier, independent] decision_refs: [D-175] cross_refs: [rare-vein-survey] diff --git a/wiki/corporations/saigon-consulting.md b/wiki/corporations/saigon-consulting.md index e468d63b6..c1874125f 100644 --- a/wiki/corporations/saigon-consulting.md +++ b/wiki/corporations/saigon-consulting.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Binh Trang (GJ 17) +corp_specialization: professional_services tags: [service_premium, east_reach, tractus] decision_refs: [D-189] cross_refs: [kovalev-freight] diff --git a/wiki/corporations/salud-alliance.md b/wiki/corporations/salud-alliance.md index a0c77d4cf..ab67b6c90 100644 --- a/wiki/corporations/salud-alliance.md +++ b/wiki/corporations/salud-alliance.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Matamba (GJ 884) +corp_specialization: trade_distribution tags: [medical_goods, chemicals, chemical_feedstock, south_reach, tractus, assembly, distributor] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/sato-medical.md b/wiki/corporations/sato-medical.md index 0d6c929bb..8680410d6 100644 --- a/wiki/corporations/sato-medical.md +++ b/wiki/corporations/sato-medical.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Oshima (GJ 848) +corp_specialization: general_industrial tags: [medical_goods, implant_hardware, east_reach, japanese, assembly] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/scapa-flow-industries.md b/wiki/corporations/scapa-flow-industries.md index cac314711..92bdbda45 100644 --- a/wiki/corporations/scapa-flow-industries.md +++ b/wiki/corporations/scapa-flow-industries.md @@ -9,6 +9,7 @@ updated: 2026-05-02 scope: reach-wide faction_type: economic headquarters: Quaterna (GJ 570A) +corp_specialization: general_industrial tags: [fusion_fuel, structural_panels, manufacturing, tractus] decision_refs: [D-175] cross_refs: [GJ-570A] diff --git a/wiki/corporations/schwarzwald-gin.md b/wiki/corporations/schwarzwald-gin.md index 7ff501dcc..471929007 100644 --- a/wiki/corporations/schwarzwald-gin.md +++ b/wiki/corporations/schwarzwald-gin.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Dunkelholz (GJ 615) +corp_specialization: terroir_spirits tags: [terroir, west_reach, mark, scarcity_class_rare] decision_refs: [D-189] cross_refs: [rheingold-distillers, svanevann-waters, mercado-travessia] diff --git a/wiki/corporations/sede-chemical-works.md b/wiki/corporations/sede-chemical-works.md index fdd2669a5..c2a66e170 100644 --- a/wiki/corporations/sede-chemical-works.md +++ b/wiki/corporations/sede-chemical-works.md @@ -9,6 +9,7 @@ updated: 2026-05-02 scope: GJ 559B local; ACB (Alpha Centauri B) corridor secondary faction_type: economic headquarters: Sede (GJ 559B) +corp_specialization: general_industrial tags: [chemical_feedstock, chemicals, manufacturing, tractus] decision_refs: [D-175] cross_refs: [societe-chimique] diff --git a/wiki/corporations/sendai-forge.md b/wiki/corporations/sendai-forge.md index 21f6e02ed..541ad1f1b 100644 --- a/wiki/corporations/sendai-forge.md +++ b/wiki/corporations/sendai-forge.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Kurashiki (GJ 205) +corp_specialization: general_industrial tags: [heritage_craft, east_reach, tractus, artisan] decision_refs: [D-189] cross_refs: [pembrokeshire-smithy, hangang-studio, higashiyama-vehicle] diff --git a/wiki/corporations/sertanejo-design.md b/wiki/corporations/sertanejo-design.md index dda412f1d..959bed6bd 100644 --- a/wiki/corporations/sertanejo-design.md +++ b/wiki/corporations/sertanejo-design.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Roça Nova (GJ 67) +corp_specialization: general_industrial tags: [design_heritage, south_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [quebrada-cultural, rijdbaar, baia-fundo-productions] diff --git a/wiki/corporations/shetland-wool.md b/wiki/corporations/shetland-wool.md index 527eb12a5..5845822c3 100644 --- a/wiki/corporations/shetland-wool.md +++ b/wiki/corporations/shetland-wool.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Crown's Hollow (GJ 661A) +corp_specialization: terroir_organics tags: [textiles, heritage_craft, north_reach, tractus, artisan] decision_refs: [D-175, D-189] cross_refs: [orkney-ceramics, highland-cooperative, thrds] diff --git a/wiki/corporations/societe-chimique.md b/wiki/corporations/societe-chimique.md index 2632359db..132c7a6c8 100644 --- a/wiki/corporations/societe-chimique.md +++ b/wiki/corporations/societe-chimique.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Confluent (GJ 395) +corp_specialization: general_industrial tags: [chemical_feedstock, chemicals, organic_compounds, manufacturing, tractus, assembly, french_heritage] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/somatic-futures.md b/wiki/corporations/somatic-futures.md index 5c7d42a1e..fa53e7254 100644 --- a/wiki/corporations/somatic-futures.md +++ b/wiki/corporations/somatic-futures.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Kallast (GJ 144) +corp_specialization: licensed_clinical_services tags: [medical_re-embodiment, medical_goods, longevity, tractus, assembly, ran] decision_refs: [D-175, D-174] cross_refs: [] diff --git a/wiki/corporations/sova-station-works.md b/wiki/corporations/sova-station-works.md index bd2918001..3fac37669 100644 --- a/wiki/corporations/sova-station-works.md +++ b/wiki/corporations/sova-station-works.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Sova (GJ 280B) +corp_specialization: general_industrial tags: [habitat_modules, structural_panels, manufacturing, assembly, slavic_heritage] decision_refs: [D-175] cross_refs: [station-sova, trans-reach-rail] diff --git a/wiki/corporations/spitsbergen-retreat.md b/wiki/corporations/spitsbergen-retreat.md index 194afaf90..78ce2a000 100644 --- a/wiki/corporations/spitsbergen-retreat.md +++ b/wiki/corporations/spitsbergen-retreat.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Halvøy (GJ 624) +corp_specialization: hospitality_hub tags: [service_premium, west_reach, mark] decision_refs: [D-189] cross_refs: [] diff --git a/wiki/corporations/stalownia-kowalski.md b/wiki/corporations/stalownia-kowalski.md index 2d63123f8..f9361b7a8 100644 --- a/wiki/corporations/stalownia-kowalski.md +++ b/wiki/corporations/stalownia-kowalski.md @@ -9,6 +9,7 @@ updated: 2026-03-15 scope: south_reach primary; west_reach secondary; reach-wide in extraction sectors faction_type: economic headquarters: Nowa Huta (GJ 896A) +corp_specialization: general_industrial tags: [refined_metals, advanced_alloys, structural_panels, heavy_equipment, south_reach, tractus] decision_refs: [D-175] cross_refs: [durban-engineering] diff --git a/wiki/corporations/steinbach-furniture.md b/wiki/corporations/steinbach-furniture.md index 8e23c10e8..12040fba4 100644 --- a/wiki/corporations/steinbach-furniture.md +++ b/wiki/corporations/steinbach-furniture.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Steinfeld (GJ 454) +corp_specialization: general_industrial tags: [design_heritage, west_reach, mark, consumer_goods] decision_refs: [D-189] cross_refs: [rijdbaar, vlaams-atelier] diff --git a/wiki/corporations/sunderland-precision.md b/wiki/corporations/sunderland-precision.md index 59a24e11c..c2c3e45de 100644 --- a/wiki/corporations/sunderland-precision.md +++ b/wiki/corporations/sunderland-precision.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Ashworth Junction (GJ 873) +corp_specialization: precision_tech tags: [tech_premium, north_reach, tractus, electronics] decision_refs: [D-189] cross_refs: [pembrokeshire-smithy, grampian-systems, rheintal-systems] diff --git a/wiki/corporations/sungshin-music.md b/wiki/corporations/sungshin-music.md index 484b22859..5e817f80e 100644 --- a/wiki/corporations/sungshin-music.md +++ b/wiki/corporations/sungshin-music.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Shimanami (GJ 848) +corp_specialization: media_center tags: [cultural, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [ondori-interactive, fjeldtone-music, open-corridor-stream] diff --git a/wiki/corporations/svanevann-waters.md b/wiki/corporations/svanevann-waters.md index a8b896d59..79e704076 100644 --- a/wiki/corporations/svanevann-waters.md +++ b/wiki/corporations/svanevann-waters.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Nyrheim (GJ 3737) +corp_specialization: terroir_agriculture tags: [water, terroir, west_reach, mark, scarcity_class_rare] decision_refs: [D-175, D-189] cross_refs: [rheingold-distillers, schwarzwald-gin, mercado-travessia] diff --git a/wiki/corporations/takamori-lattice-works.md b/wiki/corporations/takamori-lattice-works.md index a2bc06dd1..259aa01c8 100644 --- a/wiki/corporations/takamori-lattice-works.md +++ b/wiki/corporations/takamori-lattice-works.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Hải Cảng (GJ 136) +corp_specialization: lattice_material_source tags: [lattice_substrate, drive_cores, east_reach, japanese, vietnamese, tractus, assembly] decision_refs: [D-175, D-177] cross_refs: [] diff --git a/wiki/corporations/talbrau.md b/wiki/corporations/talbrau.md index e5636658c..71d04435b 100644 --- a/wiki/corporations/talbrau.md +++ b/wiki/corporations/talbrau.md @@ -9,6 +9,7 @@ updated: 2026-03-16 scope: west_reach local, corridor reputation faction_type: economic headquarters: Brückenau (GJ 798) +corp_specialization: terroir_agriculture tags: [] decision_refs: [] cross_refs: [] diff --git a/wiki/corporations/thalassa-resort-group.md b/wiki/corporations/thalassa-resort-group.md index 30e7fbd40..3cc9e70ff 100644 --- a/wiki/corporations/thalassa-resort-group.md +++ b/wiki/corporations/thalassa-resort-group.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Gateway (GJ 244A) +corp_specialization: hospitality_hub tags: [hospitality, fine_dining, entertainment, tractus, assembly, gateway] decision_refs: [D-175] cross_refs: [the-registry, prometheus-labs] diff --git a/wiki/corporations/the-blue-collective.md b/wiki/corporations/the-blue-collective.md index 661e5308d..08ce75214 100644 --- a/wiki/corporations/the-blue-collective.md +++ b/wiki/corporations/the-blue-collective.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Dagat (GJ 482A) +corp_specialization: terroir_organics tags: [agricultural_produce, organic_compounds, east_reach, cooperative, dagat_blue] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/the-registry.md b/wiki/corporations/the-registry.md index 855de8559..65ed304fe 100644 --- a/wiki/corporations/the-registry.md +++ b/wiki/corporations/the-registry.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Lendel (GJ 380) +corp_specialization: professional_services tags: [commercial_intelligence, information_brokerage, groombridge, independent] decision_refs: [D-175, D-181] cross_refs: [] diff --git a/wiki/corporations/thorngate-media.md b/wiki/corporations/thorngate-media.md index e67eecca1..6a0086b03 100644 --- a/wiki/corporations/thorngate-media.md +++ b/wiki/corporations/thorngate-media.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Crown's Hollow (GJ 661A) +corp_specialization: media_center tags: [cultural, north_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [reach-standard-media, ashwick-broadcasting, reach-archive-standard] diff --git a/wiki/corporations/thrds.md b/wiki/corporations/thrds.md index 783688aa8..5d1bc8b80 100644 --- a/wiki/corporations/thrds.md +++ b/wiki/corporations/thrds.md @@ -9,6 +9,7 @@ updated: 2026-03-16 scope: north_reach primary; transit corridor fashion markets secondary faction_type: economic headquarters: Braemar (GJ 475) +corp_specialization: general_industrial tags: [textiles, clothing, north_reach, tractus, cooperative] decision_refs: [D-175] cross_refs: [calloway-distillery, mercado-travessia] diff --git a/wiki/corporations/threshold-fuel-syndicate.md b/wiki/corporations/threshold-fuel-syndicate.md index 44ce7a511..6f5739068 100644 --- a/wiki/corporations/threshold-fuel-syndicate.md +++ b/wiki/corporations/threshold-fuel-syndicate.md @@ -9,6 +9,7 @@ updated: 2026-05-02 scope: tau_ceti local faction_type: economic headquarters: Threshold (GJ 71) +corp_specialization: fuel_production tags: [fusion_fuel, water, ice_harvesting, independent] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/tongyeong-drive.md b/wiki/corporations/tongyeong-drive.md index d3617e0fe..3101431b6 100644 --- a/wiki/corporations/tongyeong-drive.md +++ b/wiki/corporations/tongyeong-drive.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Miryang (GJ 754) +corp_specialization: precision_tech tags: [drive_cores, tech_premium, east_reach, tractus, electronics] decision_refs: [D-175, D-189] cross_refs: [kumho-navigation, higashiyama-vehicle, dalbit-systems] diff --git a/wiki/corporations/trans-reach-rail.md b/wiki/corporations/trans-reach-rail.md index e21ec38ce..997ecee48 100644 --- a/wiki/corporations/trans-reach-rail.md +++ b/wiki/corporations/trans-reach-rail.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: reach-wide faction_type: economic headquarters: Gateway (GJ 244A) +corp_specialization: general_industrial tags: [rail_infrastructure, heavy_equipment, tractus, assembly, gateway] decision_refs: [D-175] cross_refs: [mastroianni-vehicle-group, sova-station-works] diff --git a/wiki/corporations/transit-library.md b/wiki/corporations/transit-library.md index d16c0310d..1358138e8 100644 --- a/wiki/corporations/transit-library.md +++ b/wiki/corporations/transit-library.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: media_center tags: [platform_catalogue, core, tractus, entertainment] decision_refs: [D-189] cross_refs: [open-corridor-stream, resonance-premium, reach-archive-standard, groombridge-settlement-house] diff --git a/wiki/corporations/trek-standard.md b/wiki/corporations/trek-standard.md index 8e1aeed27..661412ba0 100644 --- a/wiki/corporations/trek-standard.md +++ b/wiki/corporations/trek-standard.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Groombridge (GJ 380) +corp_specialization: general_industrial tags: [commodity_branded, core, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [groombridge-settlement-house, mercado-travessia, societe-chimique] diff --git a/wiki/corporations/umoja-freight.md b/wiki/corporations/umoja-freight.md index c553750cc..54242ba07 100644 --- a/wiki/corporations/umoja-freight.md +++ b/wiki/corporations/umoja-freight.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Zenzele Horizon (GJ 1111) +corp_specialization: trade_distribution tags: [metallic_ore, refined_metals, freight, south_reach, tractus, assembly, zulu_heritage] decision_refs: [D-175] cross_refs: [] diff --git a/wiki/corporations/veldfontein-botanical.md b/wiki/corporations/veldfontein-botanical.md index 8e6900f80..258a70a7f 100644 --- a/wiki/corporations/veldfontein-botanical.md +++ b/wiki/corporations/veldfontein-botanical.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Inhambane (GJ 54) +corp_specialization: terroir_agriculture tags: [terroir, south_reach, tractus, scarcity_class_rare] decision_refs: [D-189] cross_refs: [algarve-botanics, cape-modern, salud-alliance] diff --git a/wiki/corporations/vins-de-grand-vide.md b/wiki/corporations/vins-de-grand-vide.md index 4959109de..085cd6000 100644 --- a/wiki/corporations/vins-de-grand-vide.md +++ b/wiki/corporations/vins-de-grand-vide.md @@ -9,6 +9,7 @@ updated: 2026-03-15 scope: west_reach primary; inner-corridor premium market secondary faction_type: economic headquarters: Confluent (GJ 395) +corp_specialization: terroir_agriculture tags: [] decision_refs: [] cross_refs: [nordmark-skog] diff --git a/wiki/corporations/vlaams-atelier.md b/wiki/corporations/vlaams-atelier.md index a99008a44..80bdabc70 100644 --- a/wiki/corporations/vlaams-atelier.md +++ b/wiki/corporations/vlaams-atelier.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Bestevaer (GJ 103) +corp_specialization: general_industrial tags: [design_heritage, west_reach, mark, consumer_goods] decision_refs: [D-189] cross_refs: [rijdbaar, nordmark-skog, steinbach-furniture] diff --git a/wiki/corporations/vlaanderen-optica.md b/wiki/corporations/vlaanderen-optica.md index 52289b434..842d492ca 100644 --- a/wiki/corporations/vlaanderen-optica.md +++ b/wiki/corporations/vlaanderen-optica.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Dokkum (GJ 506) +corp_specialization: precision_tech tags: [tech_premium, west_reach, mark, electronics] decision_refs: [D-189] cross_refs: [rare-vein-survey, rheintal-systems, westmark-distribution] diff --git a/wiki/corporations/vuma-sound.md b/wiki/corporations/vuma-sound.md index acb0ac799..47b8c0bc8 100644 --- a/wiki/corporations/vuma-sound.md +++ b/wiki/corporations/vuma-sound.md @@ -9,6 +9,7 @@ updated: 2026-04-21 scope: regional faction_type: economic headquarters: Matamba (GJ 884) +corp_specialization: media_center tags: [cultural, south_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [] diff --git a/wiki/corporations/westfield-common.md b/wiki/corporations/westfield-common.md index 9a68091e2..04cc1c791 100644 --- a/wiki/corporations/westfield-common.md +++ b/wiki/corporations/westfield-common.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Neustadt (GJ 526) +corp_specialization: general_industrial tags: [commodity_branded, west_reach, mark, consumer_goods] decision_refs: [D-189] cross_refs: [westmark-distribution, compact-financial-exchange, kellervolk] diff --git a/wiki/corporations/westmark-distribution.md b/wiki/corporations/westmark-distribution.md index b846fcdf9..38e83d0b8 100644 --- a/wiki/corporations/westmark-distribution.md +++ b/wiki/corporations/westmark-distribution.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Rand (GJ 601A) +corp_specialization: trade_distribution tags: [freight, consumer_goods, processed_food, compact, mark, west_reach] decision_refs: [D-171, D-172, D-175] cross_refs: [] diff --git a/wiki/corporations/westphalia-heavy-works.md b/wiki/corporations/westphalia-heavy-works.md index 8299abfb8..45651bd88 100644 --- a/wiki/corporations/westphalia-heavy-works.md +++ b/wiki/corporations/westphalia-heavy-works.md @@ -9,6 +9,7 @@ updated: 2026-04-05 scope: regional faction_type: economic headquarters: Rand (GJ 601A) +corp_specialization: general_industrial tags: [heavy_equipment, vehicles, compact, mark, west_reach, german_heritage] decision_refs: [D-175, D-172] cross_refs: [erzgebirge-instruments] diff --git a/wiki/corporations/wolkenbruch-cinema.md b/wiki/corporations/wolkenbruch-cinema.md index c8978a6dd..dbd293d77 100644 --- a/wiki/corporations/wolkenbruch-cinema.md +++ b/wiki/corporations/wolkenbruch-cinema.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Neustadt (GJ 526) +corp_specialization: media_center tags: [cultural, west_reach, mark, entertainment] decision_refs: [D-189] cross_refs: [maas-rijn-entertainment, kellervolk, westfield-common] diff --git a/wiki/corporations/workshift-apparel.md b/wiki/corporations/workshift-apparel.md index 9d12dc97f..fec996adb 100644 --- a/wiki/corporations/workshift-apparel.md +++ b/wiki/corporations/workshift-apparel.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Ashfield (GJ 178) +corp_specialization: general_industrial tags: [commodity_branded, north_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [ashwick-broadcasting, baektu-mining-alliance, highland-cooperative] diff --git a/wiki/corporations/xing-yun-films.md b/wiki/corporations/xing-yun-films.md index ed5fbbcb2..b2145a8e4 100644 --- a/wiki/corporations/xing-yun-films.md +++ b/wiki/corporations/xing-yun-films.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Songjiang (GJ 625) +corp_specialization: media_center tags: [cultural, east_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [hangang-studio, mirai-entertainment, hana-creative] diff --git a/wiki/corporations/zindua-records.md b/wiki/corporations/zindua-records.md index 5737b6e52..fc29df08a 100644 --- a/wiki/corporations/zindua-records.md +++ b/wiki/corporations/zindua-records.md @@ -9,6 +9,7 @@ updated: 2026-04-21 scope: regional faction_type: economic headquarters: Zenzele (GJ 1111) +corp_specialization: media_center tags: [cultural, south_reach, tractus, entertainment] decision_refs: [D-189] cross_refs: [] diff --git a/wiki/corporations/zulu-consumer.md b/wiki/corporations/zulu-consumer.md index 4cbf395fa..dd1b0d270 100644 --- a/wiki/corporations/zulu-consumer.md +++ b/wiki/corporations/zulu-consumer.md @@ -9,6 +9,7 @@ updated: 2026-04-22 scope: regional faction_type: economic headquarters: Zenzele (GJ 1111) +corp_specialization: general_industrial tags: [commodity_branded, south_reach, tractus, consumer_goods] decision_refs: [D-189] cross_refs: [umoja-freight, baektu-mining-alliance, zindua-records] diff --git a/wiki/economics/corp_hq_placement.toml b/wiki/economics/corp_hq_placement.toml new file mode 100644 index 000000000..c3689d9d5 --- /dev/null +++ b/wiki/economics/corp_hq_placement.toml @@ -0,0 +1,198 @@ +# ========================================================================== +# Settled Reach — Corp HQ Placement Map (D-242) +# Source of truth. Compiled into corporations.hq_placement by +# import_economics.py (populate_corp_specialization), and into the +# Standalone-HQ settlement rows atlas_city_names gains by +# populate_standalone_hq_settlements (both economy_import/corporations.py). +# +# D-242: a corporate headquarters is not a city. Each corp's HQ is either: +# CityTenant — a tenant of an EXISTING city; a corp -> city link +# (corporations.headquarters_city_id), NO new settlement. +# Many corps may tenant one city (a bank and a newspaper +# share a downtown). +# Standalone — its OWN settlement, emitted into the cascade settlement +# list with its own body + position (a mine or shipyard +# sits alone). Standalone stanzas carry a second field, +# standalone_economic_role, so the emitted atlas_city_names +# row has a real economic_role (the existing 10-value +# D-195/D-197 vocabulary — manufacturing | financial | +# agricultural | extraction | service_mixed | institutional | +# transit_hub | research | military | residential) instead +# of a generic fallback. CityTenant stanzas do not carry this +# field — no settlement is emitted for them, so it would be +# dead data. +# +# This file has exactly one stanza per corp_specialization value — the +# REUSED D-237 specialization_vocabulary id-space (see +# wiki/economics/specialization_vocabulary.toml): the original 27 +# system-authored values plus 4 corp-only Services-extension values added +# alongside this file (trade_distribution, hospitality_hub, +# professional_services, licensed_clinical_services — real gaps the T-1074 +# corp categorization pass found: ~10% of corps do logistics/hospitality/ +# consulting/licensed-clinical work no original value covered; the fourth +# landed via PR #177 M1). D-242 asked for an authored "specialization -> HQ-placement +# map"; this vocabulary already exists and already carries the signal that +# decides placement (bulk_class_projected: NonPhysical vs everything else), +# so rather than invent a second, parallel corp-only taxonomy, the map below +# is this vocabulary keyed onto the two-value placement enum (D-242 +# amendment, recorded 2026-07-16 alongside T-1074). +# +# The default follows bulk_class_projected exactly: +# NonPhysical (financial/institutional/media/legal/research services) +# -> CityTenant — an office; no physical plant needed. +# Everything else (BulkSolid/BulkLiquid/PrecisionDense/Perishable — +# farms, mines, refineries, shipyards, factories) +# -> Standalone — needs land/plant of its own. +# +# hq_placement is authored EXPLICITLY per value (not computed from +# bulk_class_projected at import time) so a future ticket can flip one +# value without touching import code — three values in the Services group +# (governance_center, transit_hub, research_station) are individually +# lore-ambiguous (a "transit hub" corp could plausibly BE its own +# spaceport-town, e.g. Gateway/Wolf 359 read as whole transit-identity +# systems in the vocabulary's own descriptions) but are pinned to the +# bulk_class_projected default here for consistency; revisit case-by-case +# if a specific corp's placement reads wrong once bodies are walkable. +# +# Decisions: D-242 (this map), D-237 (the reused vocabulary + the +# bulk_class_projected signal this defaults from). Ticket: T-1074. +# ========================================================================== + +# ------------------------------------------------------------------------- +# Agricultural — Standalone (farms/estates need land) +# ------------------------------------------------------------------------- +[estate_farming] +hq_placement = "Standalone" +standalone_economic_role = "agricultural" + +[breadbasket] +hq_placement = "Standalone" +standalone_economic_role = "agricultural" + +[terroir_agriculture] +hq_placement = "Standalone" +standalone_economic_role = "agricultural" + +[terroir_spirits] +hq_placement = "Standalone" +standalone_economic_role = "agricultural" + +[terroir_organics] +hq_placement = "Standalone" +standalone_economic_role = "agricultural" + +# ------------------------------------------------------------------------- +# Energy — Standalone (extraction/refinery infrastructure) +# ------------------------------------------------------------------------- +[fuel_production] +hq_placement = "Standalone" +standalone_economic_role = "extraction" + +[geothermal_hub] +hq_placement = "Standalone" +standalone_economic_role = "extraction" + +# ------------------------------------------------------------------------- +# Extraction — Standalone (mine/quarry sits on the deposit) +# ------------------------------------------------------------------------- +[ore_extraction] +hq_placement = "Standalone" +standalone_economic_role = "extraction" + +[company_mining] +hq_placement = "Standalone" +standalone_economic_role = "extraction" + +[marble_monopoly] +hq_placement = "Standalone" +standalone_economic_role = "extraction" + +[rare_mineral_extraction] +hq_placement = "Standalone" +standalone_economic_role = "extraction" + +[lattice_material_source] +hq_placement = "Standalone" +standalone_economic_role = "extraction" + +# ------------------------------------------------------------------------- +# Manufacturing — Standalone (factory/yard footprint) +# ------------------------------------------------------------------------- +[general_industrial] +hq_placement = "Standalone" +standalone_economic_role = "manufacturing" + +[shipbuilding] +hq_placement = "Standalone" +standalone_economic_role = "manufacturing" + +[vehicle_production] +hq_placement = "Standalone" +standalone_economic_role = "manufacturing" + +[consumer_goods_bazaar] +hq_placement = "Standalone" +standalone_economic_role = "manufacturing" + +[gate_fabrication] +hq_placement = "Standalone" +standalone_economic_role = "manufacturing" + +[military_industrial] +hq_placement = "Standalone" +standalone_economic_role = "military" + +# ------------------------------------------------------------------------- +# High-tech — Standalone (fabrication/aquaculture plant) +# ------------------------------------------------------------------------- +[precision_tech] +hq_placement = "Standalone" +standalone_economic_role = "manufacturing" + +[marine_farming] +hq_placement = "Standalone" +standalone_economic_role = "agricultural" + +# ------------------------------------------------------------------------- +# Services (all NonPhysical) — CityTenant (an office, no plant); no +# standalone_economic_role — no settlement is ever emitted for these. +# ------------------------------------------------------------------------- +[financial_hub] +hq_placement = "CityTenant" + +[governance_center] +hq_placement = "CityTenant" + +[transit_hub] +hq_placement = "CityTenant" + +[research_station] +hq_placement = "CityTenant" + +[legal_archive] +hq_placement = "CityTenant" + +[media_center] +hq_placement = "CityTenant" + +[longevity_monopoly] +hq_placement = "CityTenant" + +# ------------------------------------------------------------------------- +# Services extension (4, all NonPhysical) — added alongside the vocabulary +# extension in specialization_vocabulary.toml (D-242/T-1074; the fourth via +# PR #177 M1). Same rule as the rest of Services: NonPhysical -> CityTenant +# (an office/depot/clinic, not a factory — this is exactly what made these +# corps hard to place in the original 27). +# ------------------------------------------------------------------------- +[trade_distribution] +hq_placement = "CityTenant" + +[hospitality_hub] +hq_placement = "CityTenant" + +[professional_services] +hq_placement = "CityTenant" + +[licensed_clinical_services] +hq_placement = "CityTenant" diff --git a/wiki/economics/settlement_name_locked.toml b/wiki/economics/settlement_name_locked.toml new file mode 100644 index 000000000..61b658805 --- /dev/null +++ b/wiki/economics/settlement_name_locked.toml @@ -0,0 +1,94 @@ +# ========================================================================== +# Settled Reach — Authored NameLocked Settlement Overrides (D-242, T-1075) +# Source of truth. Compiled by import_economics.py +# (atlas.populate_settlement_population_class) onto atlas_city_names.settlement_class. +# +# D-196 defines four SettlementClass variants. Bake scope (T-1075) only +# assigns two of them: every pooled city defaults to PopulationBudget (the +# D-196 population thresholds — >=50k active / <5k ghost — are read from +# this baked population by a later, not-yet-built GENERATION-time consumer; +# out of this bake's scope); this file is the small authored override list +# that PINS a settlement to NameLocked instead — "named in wiki; always +# active regardless of population" (D-196). EconomicTriggered and +# OrganicGrowth are simulation-time classes (D-196) — out of bake scope, +# never written here. +# +# Use sparingly. NameLocked exists for settlements whose narrative +# significance does not depend on (and must survive) whatever population +# number the rank-size bake happens to produce — capitals and the handful of +# systems with a singular landmark identity (D-237's must-pin hero systems: +# docs/workshops/system-economic-specialization/workshop-outcomes.md §7). +# +# REVISED 2026-07-16 after the first live make regen-db (T-1074+T-1075 +# integration test): the first draft of this file picked names straight out +# of the PRE-T-1074 atlas_city_names table (Sirius, Groombridge, Cygni B, +# Renaissance, Parallax, Prometheus, Nova Roma) — those turned out to be +# corp-HQ cross-reference ARTIFACTS from the retired D-207 +# populate_atlas_city_names_corps (the corp's authored HQ-city text +# frequently equalled its system's proper_name, e.g. Gate Corp's +# "headquarters: Renaissance (GJ 251)"). Once that insert path was removed +# (D-242's whole point), those names no longer exist in the pure +# markers.json pool. Every entry below is verified against real, repeated +# `make regen-db` runs — see the note on each stanza for its actual source +# row. Note (PR #177 H1/H3): corporations.headquarters_body is RECOMPUTED +# from source on every run now, so the three corp-named Standalone-HQ pins +# below (Cygni Combines, The Gate Corporation, Mastroianni Vehicle Group) +# hold because their HQ bodies carry real authored population — the +# population-first tiebreak re-derives the same body every run (verified: +# two consecutive regens, all 8 pins applied, zero placement drift) — not +# because a stale first-run value is being kept. Prometheus (GJ 702B) is +# DROPPED from this list — verified live: every one of its 12 bodies has +# zero authored markers.json city names AND zero population, so there is +# no settlement to pin at all (a genuine wiki content gap: the Reach's +# longevity-monopoly hero system has no named city anywhere — T-1115 +# authors the missing names; the bake cannot invent a name to pin). +# +# Match key: (body_id, name) against atlas_city_names — NOT the numeric +# atlas_city_names.id, which is regen-volatile (AUTOINCREMENT, no +# UNIQUE(body_id, name) — see the atlas_city_names schema comment, D-242). +# A stanza with no matching row is a silent no-op (logged) rather than an +# import error, so a future markers.json rename doesn't hard-fail regen-db. +# +# Decisions: D-196 (SettlementClass), D-242 (population/class baking scope), +# D-237 (the hero-system set these are drawn from). Ticket: T-1075. +# ========================================================================== + +[[hero]] +body_id = "GJ144d" +name = "Kallast" +note = "Kallast / GJ 144 (Ran) — THE breadbasket; lore-anchored plains body (D-239 §1). Pure markers.json pool name, unaffected by the corp-HQ-artifact issue." + +[[hero]] +body_id = "GJ820Bc" +name = "Cygni Combines" +note = "Cygni B / GJ 820B — shipbuilding cluster, 7 competing combines (D-237). D-242 Standalone-HQ settlement (general_industrial corp_specialization) — the corp's own settlement IS the hero identity here; the pure pool has only 'Metropolis' (generic)." + +[[hero]] +body_id = "GJ251c" +name = "The Gate Corporation" +note = "Renaissance / GJ 251 — Gate Corp fabrication monopoly (D-237). D-242 Standalone-HQ settlement (gate_fabrication, MonopolySource) — the pure pool has only 'Tributarium'/'Ruhr' (generic); the monopolist's own company town is the hero place." + +[[hero]] +body_id = "GJ764d" +name = "Mastroianni Vehicle Group" +note = "Nova Roma / GJ 764 — MVG vehicle production (D-237). D-242 Standalone-HQ settlement (vehicle_production) — pure pool has only 'Mirafiori'/'Fiorino' (generic)." + +[[hero]] +body_id = "GJ380c" +name = "Aldren" +note = "Groombridge / GJ 380 — financial cluster + GSH clearing-house landmark (D-237). D-237's hero identity is the SERVICES/finance character, not any one CityTenant corp's manufacturing HQ — pinned to the lowest-id pure-pool city (a CityTenant host) rather than a Standalone-HQ settlement, since Groombridge's Standalone HQs (Alcyone Tech, Arclamp, ...) are all general_industrial/precision_tech, not financial_hub." + +[[hero]] +body_id = "GJ244Ad" +name = "Mandate" +note = "Sirius / GJ 244A — 'the capital' (system_specialization.toml). Lowest-id pure-pool city; the pure pool no longer contains 'Sirius' itself (that was a corp-HQ artifact)." + +[[hero]] +body_id = "GJ280Ad" +name = "Strata" +note = "Parallax / GJ 280A — news syndicates / cultural production center (D-237). Lowest-id pure-pool city (CityTenant host for the media_center specialization)." + +[[hero]] +body_id = "GJ338Bd" +name = "Viridarium" +note = "Arbour / GJ 338B — estate amenity agriculture (D-237). Lowest-id pure-pool city; reads as the manor/estate theme D-237 describes." diff --git a/wiki/economics/specialization_vocabulary.toml b/wiki/economics/specialization_vocabulary.toml index a22d006a8..b52ff7c03 100644 --- a/wiki/economics/specialization_vocabulary.toml +++ b/wiki/economics/specialization_vocabulary.toml @@ -1,5 +1,5 @@ # ========================================================================== -# Settled Reach — Economic Specialization Vocabulary (D-237) +# Settled Reach — Economic Specialization Vocabulary (D-237, extended D-242) # Source of truth. Compiled to systems.db specialization_vocabulary by # import_economics.py. # @@ -8,6 +8,18 @@ # SCALE is encoded in the value itself (breadbasket vs estate_farming vs # terroir_agriculture) — there is no separate scale column. # +# SHARED VOCABULARY (D-242, T-1074): this id-space is used by TWO axes — +# system_economy.economic_specialization (D-237, the original per-SYSTEM +# authored identity) and corporations.corp_specialization (D-242, the +# per-CORP HQ-placement key; see corp_hq_placement.toml). A corp's +# specialization is authored directly in its wiki-page frontmatter +# (wiki/corporations/*.md, key: corp_specialization), not in a separate +# per-corp TOML — one value in this table, two authored sources reading it. +# The 4-value "Services extension" block near the end of the Services +# section was added for corp_specialization coverage (corp-only gaps the +# original 27 system-scale values didn't need to cover); nothing stops a +# future system from adopting them too if the fit is right. +# # Fields per entry: # commodity_id FK -> commodities.commodity_id (the anchor # commodity; also drives body-level draws) @@ -252,3 +264,50 @@ production_ubiquity_override = "monopolistic" bulk_class_projected = "NonPhysical" production_ubiquity_projected = "MonopolySource" description = "The single licensed longevity/re-embodiment facility; access-controlled (Prometheus)." + +# ------------------------------------------------------------------------- +# Services extension (4) — added D-242/T-1074 for corp_specialization reuse +# (first three), extended with licensed_clinical_services per PR #177 M1. +# +# The categorization pass that backfilled corp_specialization for all 165 +# corporations (T-1074) found ~10% of wiki corps whose actual business +# — moving/selling already-made goods, running hospitality/tourism +# properties, general professional advisory, or licensed multi-site +# clinical care — had no honest fit among the original 27 system-authored +# values above. Per D-242's amendment ("prefer reusing/extending the +# existing vocabulary over inventing a parallel taxonomy"), these values +# extend the shared vocabulary rather than spawn a corp-only one. All four +# are NonPhysical (services; CityTenant HQ placement per +# corp_hq_placement.toml) even though trade_distribution's anchor commodity +# is itself physical — the SPECIALIZATION is the logistics/service +# activity, not manufacture, which is exactly the distinction that made +# these corps ungroupable in the first place (a distributor's HQ is an +# office/depot, not a factory). +# ------------------------------------------------------------------------- +[trade_distribution] +commodity_id = "consumer_goods" +production_ubiquity_override = "" # catalog default (ubiquitous) — distribution touches everything +bulk_class_projected = "NonPhysical" +production_ubiquity_projected = "Common" +description = "Wholesale/retail trade and freight distribution — moves goods at scale, does not manufacture them (Mercado Travessia, Eastbay Trading)." + +[hospitality_hub] +commodity_id = "hospitality" +production_ubiquity_override = "" # catalog default (regional) +bulk_class_projected = "NonPhysical" +production_ubiquity_projected = "Specialist" +description = "Lodging, resort, spa, and recovery-stay operators; tourism/hospitality is the settlement's economic identity (Alcazar Hospitality, Thalassa Resort Group)." + +[professional_services] +commodity_id = "insurance" +production_ubiquity_override = "" # catalog default (regional) +bulk_class_projected = "NonPhysical" +production_ubiquity_projected = "Common" +description = "General business/strategy/operational advisory — risk, consulting, arbitration outside the dedicated legal_archive/financial_hub identities (Saigon Consulting, Lusaka Advisory)." + +[licensed_clinical_services] +commodity_id = "medical_services" +production_ubiquity_override = "concentrated" +bulk_class_projected = "NonPhysical" +production_ubiquity_projected = "Specialist" +description = "Licensed multi-site clinical network — Assembly-certified medical/re-embodiment operators, one of several (Somatic Futures, 40+ clinics); the non-monopoly counterpart to longevity_monopoly (Prometheus, THE sole facility)."