fix(db): PR #177 review round — H1/H2/T1/T2/M1
H1+H2: headquarters_body is reset+derived every run (DB is never source); optional authored frontmatter override, hard-validated; tiebreak now population DESC -> city-bearing body -> type rank -> body_id, preserving belt tenancies and fixing GJ702B to GJ702Bb. T2: NULL-reset pass for corp_specialization/hq_placement before authored re-apply (poison-tested). T1: wiki/corporations/*.md globbed into IMPORT_ECONOMICS_SOURCES. M1: licensed_clinical_services vocabulary value (31st, NonPhysical->CityTenant) + somatic-futures retag. Fixpoint verified stable across 4 consecutive regens (0 diffs); regen systems.db. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)."
|
||||
|
||||
Reference in New Issue
Block a user