diff --git a/server/data/systems.db b/server/data/systems.db index 1c900a5ab..3080f69a6 100644 Binary files a/server/data/systems.db and b/server/data/systems.db differ diff --git a/tooling/economy-db/economy_import/atlas.py b/tooling/economy-db/economy_import/atlas.py index 308418c66..7781f4cfd 100644 --- a/tooling/economy-db/economy_import/atlas.py +++ b/tooling/economy-db/economy_import/atlas.py @@ -164,17 +164,18 @@ def populate_atlas_city_names(conn: sqlite3.Connection, dry_run: bool) -> int: return len(rows) -# body_type preference for the most_populated_body_in_system tiebreak — lower -# sorts first. Only breaks a population TIE (real population always wins); -# this exists because a plain population-then-body_id sort can otherwise land -# on an asteroid_belt/oort_cloud sibling purely because '-' (the body_id -# naming convention for these types, e.g. "GJ702B-belt") sorts before letters -# — a body type that structurally cannot host a settlement (found live in -# T-1074/T-1075 regen: GJ 702B's 12 bodies are ALL population=0, and the old -# alphabetical-only tiebreak picked "GJ702B-belt" over 6 sibling planets, -# stranding prometheus-labs/kovalev-freight's CityTenant resolution with a -# city-less body). planet/moon/gas_giant can plausibly carry cities or -# stations; asteroid_belt/oort_cloud are geometry, never named settlements. +# 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, @@ -184,23 +185,39 @@ _BODY_TYPE_SETTLEMENT_PREFERENCE: dict[str, int] = { } -def most_populated_body_in_system(conn: sqlite3.Connection, system_id: str) -> str | None: - """Return the most-populated body_id in `system_id`, or None if the system - has no bodies. +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: this is the heuristic the retired + 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 — lifted out here (rather than duplicated) so - `populate_corp_specialization` (economy_import/corporations.py) can reuse - it to backfill `corporations.headquarters_body` for Standalone HQs. + 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): (1) population descending — - real population always wins regardless of type; (2) body_type preference - (planet < moon < gas_giant < asteroid_belt/oort_cloud — see - `_BODY_TYPE_SETTLEMENT_PREFERENCE`) — only engages among population TIES, - so it never overrides an authored population signal, it only picks a - saner body among zero-population siblings; (3) body_id ascending, the - final tiebreak when population AND type both tie. + 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. """ rows = conn.execute( "SELECT body_id, body_type, COALESCE(population, 0) FROM bodies WHERE system_id = ?", @@ -211,7 +228,8 @@ def most_populated_body_in_system(conn: sqlite3.Connection, system_id: str) -> s rows.sort( key=lambda r: ( -r[2], # population descending - _BODY_TYPE_SETTLEMENT_PREFERENCE.get(r[1], 3), # body_type preference + 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 ) ) diff --git a/tooling/economy-db/economy_import/corporations.py b/tooling/economy-db/economy_import/corporations.py index 576de28af..1bf88c389 100644 --- a/tooling/economy-db/economy_import/corporations.py +++ b/tooling/economy-db/economy_import/corporations.py @@ -57,9 +57,16 @@ def load_wiki_corps() -> list[dict]: 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 30 values for corp coverage; see + # 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"], @@ -67,6 +74,7 @@ def load_wiki_corps() -> list[dict]: "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 @@ -191,42 +199,65 @@ def _resolve_hq_location( 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). + """Phase A of the D-242 corp-HQ model (T-1074; reworked per PR #177 H1/H2/T2). - 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 + 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 backfill - `corporations.headquarters_body` via the most-populated-body-in-system - heuristic (`atlas.most_populated_body_in_system` — lifted from the retired - `populate_atlas_city_names_corps`, D-207/#909) for any corp that doesn't - already have one authored. + '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` to resolve corp_presence locations, #909's existing - resolution order) — so `import_corp_presence` picks up the backfilled - value for free, with no change to its own logic. + `corporations`) and BEFORE step 12 (see presence timing above). - Does NOT touch `atlas_city_names` — this is Phase A. Phase B - (`populate_standalone_hq_settlements`) runs after `populate_atlas_city_names` - and emits Standalone-HQ settlement rows + resolves CityTenant - `headquarters_city_id` links, since both need the city pool to exist. + 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. - HARD error (raises ImportAborted) if any corp's authored - `corp_specialization` is non-empty but not a valid vocabulary id, OR if - any corp's specialization does not resolve to a placement value in - `corp_hq_placement.toml` (a vocabulary/placement-map drift — every - vocabulary id must have a placement stanza). A corp with NO authored - `corp_specialization` is a soft warning (defers headquarters_body - backfill and Phase B placement for that corp) — not fatal, so an - un-authored future corp doesn't block regen-db; but ALL 165 corps in the - live wiki are authored as of T-1074, so this path is not expected to fire - on the shipped data. - - Returns a coverage dict: {"total", "specialized", "placed", "backfilled_hq"}. + 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) @@ -234,12 +265,26 @@ def populate_corp_specialization( 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) rows to write. - spec_rows: list[tuple[str, str, 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"] @@ -267,7 +312,25 @@ def populate_corp_specialization( f"(expected 'CityTenant' or 'Standalone')" ) continue - spec_rows.append((corp_id, spec, placement)) + # 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)}):") @@ -278,45 +341,51 @@ def populate_corp_specialization( if unauthored: print( f" warning: {len(unauthored)} corp(s) have no authored " - f"corp_specialization (headquarters_body/placement skipped): {unauthored[:5]}" + f"corp_specialization (stay NULL; no HQ placement): {unauthored[:5]}" ) - n_backfilled = 0 - if not dry_run: - for corp_id, spec, placement in spec_rows: - conn.execute( - "UPDATE corporations SET corp_specialization = ?, hq_placement = ? " - "WHERE corp_id = ?", - (spec, placement, corp_id), - ) - # Backfill headquarters_body — only for rows that don't already have one - # (never clobber an authored value; today none are authored, but the - # column exists precisely so a future ticket CAN author one directly). - for corp_id, _spec, _placement in spec_rows: - row = conn.execute( - "SELECT headquarters_system, headquarters_body FROM corporations WHERE corp_id = ?", + # 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() - if row is None: - continue - hq_system, hq_body = row - if hq_body: - continue - if not hq_system: - continue - resolved = most_populated_body_in_system(conn, hq_system) - if resolved: - conn.execute( - "UPDATE corporations SET headquarters_body = ? WHERE corp_id = ?", - (resolved, corp_id), - ) - n_backfilled += 1 + 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), - "placed": len(spec_rows), - "backfilled_hq": n_backfilled, + "hq_resolved": n_resolved, + "hq_overridden": n_overridden, } @@ -403,7 +472,7 @@ def populate_standalone_hq_settlements(conn: sqlite3.Connection, dry_run: bool) 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 30-value specialization_vocabulary, which `match_cities`/ + — 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. diff --git a/tooling/economy-db/economy_import/paths.py b/tooling/economy-db/economy_import/paths.py index 8e905c88b..fb9557227 100644 --- a/tooling/economy-db/economy_import/paths.py +++ b/tooling/economy-db/economy_import/paths.py @@ -14,6 +14,7 @@ from generator_sources import ( ARCHITECTURE_ZONE_BIAS_TOML, COLOR_REGISTER_BANDS_TOML, CORP_HQ_PLACEMENT_TOML, + CORPORATIONS_DIR, GENERATE_BRANDS_WRAPPER, OBJECT_TAG_VOCABULARY_TOML, REPO_ROOT, @@ -52,7 +53,9 @@ COMMODITIES_TOML: Path = REPO_ROOT / "wiki" / "economics" / "commodities.toml" CHAINS_TOML: Path = REPO_ROOT / "wiki" / "economics" / "production_chains.toml" CURRENCY_ZONES_TOML: Path = REPO_ROOT / "wiki" / "economics" / "currency_zones.toml" SCHEMA_SQL: Path = REPO_ROOT / "server" / "data" / "systems-schema.sql" -CORPORATIONS_DIR: Path = REPO_ROOT / "wiki" / "corporations" +# CORPORATIONS_DIR moved to generator_sources.py (T1, PR #177): the corp pages +# are now part of the stamped source set, and stamped paths are defined once +# in the registry — re-exported via the import block above. WIKI_STAR_SYSTEMS: Path = REPO_ROOT / "wiki" / "star-systems" BRANDS_TOML: Path = REPO_ROOT / "wiki" / "economics" / "corporations" / "brands.toml" GENERATED_BRANDS_TOML: Path = ( diff --git a/tooling/economy-db/import_economics.py b/tooling/economy-db/import_economics.py index 9736dcdb6..cc05b274d 100755 --- a/tooling/economy-db/import_economics.py +++ b/tooling/economy-db/import_economics.py @@ -183,13 +183,19 @@ def main() -> None: print(f" {n_db_corps} corporations in DB ({len(wiki_corps)} from wiki)") # 7b. Corp specialization + HQ placement (D-242, T-1074) — Phase A. - # Must run after corp sync (rows must exist) and before corp_presence - # (which reads headquarters_body back off corporations). + # 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['backfilled_hq']} headquarters_body backfilled" + 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 diff --git a/tooling/generator_sources.py b/tooling/generator_sources.py index 0ff97e4c3..647394427 100644 --- a/tooling/generator_sources.py +++ b/tooling/generator_sources.py @@ -79,6 +79,14 @@ SYSTEM_SPECIALIZATION_TOML: Path = ( 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 = ( @@ -127,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, diff --git a/wiki/corporations/somatic-futures.md b/wiki/corporations/somatic-futures.md index e36a1ced4..fa53e7254 100644 --- a/wiki/corporations/somatic-futures.md +++ b/wiki/corporations/somatic-futures.md @@ -9,7 +9,7 @@ updated: 2026-04-05 scope: reach-wide faction_type: economic headquarters: Kallast (GJ 144) -corp_specialization: professional_services +corp_specialization: licensed_clinical_services tags: [medical_re-embodiment, medical_goods, longevity, tractus, assembly, ran] decision_refs: [D-175, D-174] cross_refs: [] diff --git a/wiki/economics/corp_hq_placement.toml b/wiki/economics/corp_hq_placement.toml index 2b1ec9e32..c3689d9d5 100644 --- a/wiki/economics/corp_hq_placement.toml +++ b/wiki/economics/corp_hq_placement.toml @@ -25,11 +25,12 @@ # This file has exactly one stanza per corp_specialization value — the # REUSED D-237 specialization_vocabulary id-space (see # wiki/economics/specialization_vocabulary.toml): the original 27 -# system-authored values plus 3 corp-only Services-extension values added +# system-authored values plus 4 corp-only Services-extension values added # alongside this file (trade_distribution, hospitality_hub, -# professional_services — a real gap the T-1074 corp categorization pass -# found: ~10% of corps do logistics/hospitality/consulting work no original -# value covered). D-242 asked for an authored "specialization -> HQ-placement +# professional_services, licensed_clinical_services — real gaps the T-1074 +# corp categorization pass found: ~10% of corps do logistics/hospitality/ +# consulting/licensed-clinical work no original value covered; the fourth +# landed via PR #177 M1). D-242 asked for an authored "specialization -> HQ-placement # map"; this vocabulary already exists and already carries the signal that # decides placement (bulk_class_projected: NonPhysical vs everything else), # so rather than invent a second, parallel corp-only taxonomy, the map below @@ -178,11 +179,11 @@ hq_placement = "CityTenant" hq_placement = "CityTenant" # ------------------------------------------------------------------------- -# Services extension (3, all NonPhysical) — added alongside the vocabulary -# extension in specialization_vocabulary.toml (D-242/T-1074). Same rule as -# the rest of Services: NonPhysical -> CityTenant (an office/depot, not a -# factory — this is exactly what made these corps hard to place in the -# original 27). +# Services extension (4, all NonPhysical) — added alongside the vocabulary +# extension in specialization_vocabulary.toml (D-242/T-1074; the fourth via +# PR #177 M1). Same rule as the rest of Services: NonPhysical -> CityTenant +# (an office/depot/clinic, not a factory — this is exactly what made these +# corps hard to place in the original 27). # ------------------------------------------------------------------------- [trade_distribution] hq_placement = "CityTenant" @@ -192,3 +193,6 @@ hq_placement = "CityTenant" [professional_services] hq_placement = "CityTenant" + +[licensed_clinical_services] +hq_placement = "CityTenant" diff --git a/wiki/economics/settlement_name_locked.toml b/wiki/economics/settlement_name_locked.toml index 42ab64393..61b658805 100644 --- a/wiki/economics/settlement_name_locked.toml +++ b/wiki/economics/settlement_name_locked.toml @@ -28,14 +28,20 @@ # frequently equalled its system's proper_name, e.g. Gate Corp's # "headquarters: Renaissance (GJ 251)"). Once that insert path was removed # (D-242's whole point), those names no longer exist in the pure -# markers.json pool. Every entry below was re-verified against a real -# `make regen-db` output — see the note on each stanza for its actual -# source row. Prometheus (GJ 702B) is DROPPED from this list — verified -# live: every one of its 12 bodies has zero authored markers.json city -# names AND zero population, so there is no settlement to pin at all (a -# genuine wiki content gap: the Reach's longevity-monopoly hero system has -# no named city anywhere in its system — flagged for the lead, not fixed -# here; population/settlement_class baking cannot invent a name to pin). +# markers.json pool. Every entry below is verified against real, repeated +# `make regen-db` runs — see the note on each stanza for its actual source +# row. Note (PR #177 H1/H3): corporations.headquarters_body is RECOMPUTED +# from source on every run now, so the three corp-named Standalone-HQ pins +# below (Cygni Combines, The Gate Corporation, Mastroianni Vehicle Group) +# hold because their HQ bodies carry real authored population — the +# population-first tiebreak re-derives the same body every run (verified: +# two consecutive regens, all 8 pins applied, zero placement drift) — not +# because a stale first-run value is being kept. Prometheus (GJ 702B) is +# DROPPED from this list — verified live: every one of its 12 bodies has +# zero authored markers.json city names AND zero population, so there is +# no settlement to pin at all (a genuine wiki content gap: the Reach's +# longevity-monopoly hero system has no named city anywhere — T-1115 +# authors the missing names; the bake cannot invent a name to pin). # # Match key: (body_id, name) against atlas_city_names — NOT the numeric # atlas_city_names.id, which is regen-volatile (AUTOINCREMENT, no diff --git a/wiki/economics/specialization_vocabulary.toml b/wiki/economics/specialization_vocabulary.toml index 77cf9af83..b52ff7c03 100644 --- a/wiki/economics/specialization_vocabulary.toml +++ b/wiki/economics/specialization_vocabulary.toml @@ -15,8 +15,8 @@ # specialization is authored directly in its wiki-page frontmatter # (wiki/corporations/*.md, key: corp_specialization), not in a separate # per-corp TOML — one value in this table, two authored sources reading it. -# The 3-value "Services extension" block near the end of the Services -# section was added for corp_specialization coverage (a corp-only gap the +# The 4-value "Services extension" block near the end of the Services +# section was added for corp_specialization coverage (corp-only gaps the # original 27 system-scale values didn't need to cover); nothing stops a # future system from adopting them too if the fit is right. # @@ -266,21 +266,23 @@ production_ubiquity_projected = "MonopolySource" description = "The single licensed longevity/re-embodiment facility; access-controlled (Prometheus)." # ------------------------------------------------------------------------- -# Services extension (3) — added D-242/T-1074 for corp_specialization reuse. +# Services extension (4) — added D-242/T-1074 for corp_specialization reuse +# (first three), extended with licensed_clinical_services per PR #177 M1. # # The categorization pass that backfilled corp_specialization for all 165 -# corporations (T-1074) found 16/155 wiki corps (~10%) whose actual business +# corporations (T-1074) found ~10% of wiki corps whose actual business # — moving/selling already-made goods, running hospitality/tourism -# properties, or general professional advisory — had no honest fit among -# the original 24 system-authored values above. Per D-242's amendment -# ("prefer reusing/extending the existing vocabulary over inventing a -# parallel taxonomy"), these three values extend the shared vocabulary -# rather than spawn a corp-only one. All three are NonPhysical (services; -# CityTenant HQ placement per corp_hq_placement.toml) even though -# trade_distribution's anchor commodity is itself physical — the -# SPECIALIZATION is the logistics/service activity, not manufacture, which -# is exactly the distinction that made these corps ungroupable in the first -# place (a distributor's HQ is an office/depot, not a factory). +# properties, general professional advisory, or licensed multi-site +# clinical care — had no honest fit among the original 27 system-authored +# values above. Per D-242's amendment ("prefer reusing/extending the +# existing vocabulary over inventing a parallel taxonomy"), these values +# extend the shared vocabulary rather than spawn a corp-only one. All four +# are NonPhysical (services; CityTenant HQ placement per +# corp_hq_placement.toml) even though trade_distribution's anchor commodity +# is itself physical — the SPECIALIZATION is the logistics/service +# activity, not manufacture, which is exactly the distinction that made +# these corps ungroupable in the first place (a distributor's HQ is an +# office/depot, not a factory). # ------------------------------------------------------------------------- [trade_distribution] commodity_id = "consumer_goods" @@ -302,3 +304,10 @@ production_ubiquity_override = "" # catalog default (regional) bulk_class_projected = "NonPhysical" production_ubiquity_projected = "Common" description = "General business/strategy/operational advisory — risk, consulting, arbitration outside the dedicated legal_archive/financial_hub identities (Saigon Consulting, Lusaka Advisory)." + +[licensed_clinical_services] +commodity_id = "medical_services" +production_ubiquity_override = "concentrated" +bulk_class_projected = "NonPhysical" +production_ubiquity_projected = "Specialist" +description = "Licensed multi-site clinical network — Assembly-certified medical/re-embodiment operators, one of several (Somatic Futures, 40+ clinics); the non-monopoly counterpart to longevity_monopoly (Prometheus, THE sole facility)."