Merge remote-tracking branch 'origin/settlement-dataset-d242'
This commit is contained in:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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'))
|
||||
);
|
||||
|
||||
Binary file not shown.
@@ -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<CityRecord> = (1..=5)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user