feat(db): biosphere_class authoring pipeline + values (T-1085 workstream 3, D-247)

The D-247 chirality/edibility register as authored data on every body, via the
stamped regen-db path (import_economics) — not a manual bake.

- systems-schema.sql + migration.py: nullable bodies.biosphere_class column.
- economy_import/bodies.py: populate_biosphere_class — reads the authored
  environment.biosphere_class frontmatter override, else the D-247 two-gate default
  (Gate 0 habitability from atmosphere/hydrosphere/inhabited; Gate 1 Compatible/Mirror
  from economic_base_primary; neutral remainder -> stable ~50/50 split). Recomputes
  every body each regen (frontmatter + default are its complete source).
- import_economics.py: wired as import step 17.
- frontmatter overrides for the bodies whose default deviates from canon:
  Solandar=NativeMirror, GJ-524=NativeMirror (uninhabited-but-alive), Ha Long=
  NativeCompatible (its transit economy hides its aquaculture identity). Linnaeus,
  Arbour, Oshima, Freyburg, Puerto Ultimo, Edict all default correctly.

systems.db regenerated + stamped. All 9 canon exemplars verified; distribution is
111 Compatible : 108 Mirror among inhabited alive worlds (~50/50 target), Airless for
uninhabited bodies, TerraformedSterile for thin-atmosphere worlds.

Follow-up (rename completion): propagating Linnaeus to bodies.proper_name in the DB
(still "Cadwal" — proper_name is atlas-CLI-owned, and a wipe+reimport would drop
non-proposal columns like founding_age_years; needs a targeted approach).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 11:56:52 +02:00
co-authored by Claude Opus 4.8
parent 5fb34675eb
commit cc7c5ada04
10 changed files with 283 additions and 1 deletions
+131
View File
@@ -202,3 +202,134 @@ def populate_axial_tilt_deg(conn: sqlite3.Connection, dry_run: bool) -> int:
)
return len(updates)
def populate_biosphere_class(conn: sqlite3.Connection, dry_run: bool) -> int:
"""Populate bodies.biosphere_class (D-247, T-1085).
Reads ``environment.biosphere_class`` from each body's index.md frontmatter (the
AUTHORED override) and, where absent, applies the D-247 two-gate default from the
body's atmosphere / hydrosphere / inhabited flag + the system's
``economic_base_primary``. Recomputes for ALL bodies each run (the value is fully
determined by frontmatter + the default, with no other authoritative writer to
preserve), so an edited override re-applies on the next regen.
Authored input, not seed entropy (D-247): the default is a function of other
AUTHORED fields, and explicit frontmatter always wins.
- Gate 0 (habitability): uninhabited or no atmosphere -> Airless; inhabited +
thin atmosphere -> engineered -> TerraformedSterile; inhabited + real
atmosphere + open liquid water -> Alive -> Gate 1; inhabited + real atmosphere
but no liquid water (ice/subsurface) -> sealed/engineered -> Airless.
- Gate 1 (Compatible vs Mirror): agriculture/service exporters -> Compatible;
penal/extraction/manufacturing -> Mirror; the neutral remainder -> a stable
body_id split toward the ~50/50 design target (canon worlds carry explicit
frontmatter overrides, so they never depend on this split).
"""
import glob as _glob
import hashlib
try:
import yaml as _yaml
_has_yaml = True
except ImportError:
_has_yaml = False
def _biosphere_from_frontmatter(text: str) -> tuple[str | None, str | None]:
"""Return ``(body_id, biosphere_class)`` from a body's frontmatter, or (None, None)."""
lines = text.split("\n")
if not lines or lines[0].strip() != "---":
return (None, None)
end = None
for i, line in enumerate(lines[1:], 1):
if line.strip() == "---":
end = i
break
if end is None:
return (None, None)
block_lines = lines[1:end]
if _has_yaml:
try:
fm = _yaml.safe_load("\n".join(block_lines))
except Exception:
fm = None
if isinstance(fm, dict):
env = fm.get("environment")
bc = env.get("biosphere_class") if isinstance(env, dict) else None
return (str(fm.get("id")) if fm.get("id") else None, str(bc) if bc else None)
return (None, None)
# Manual fallback: scan `id:` and `environment.biosphere_class`.
body_id = None
bc = None
in_env = False
for line in block_lines:
stripped = line.strip()
if stripped.startswith("id:"):
body_id = stripped.partition(":")[2].strip() or None
if stripped == "environment:":
in_env = True
continue
if in_env:
if line and not line[0].isspace():
in_env = False
elif stripped.startswith("biosphere_class:"):
bc = stripped.partition(":")[2].strip() or None
return (body_id, bc)
# 1. Authored overrides from frontmatter.
override: dict[str, str] = {}
body_files = sorted(_glob.glob(str(WIKI_STAR_SYSTEMS / "*" / "bodies" / "*" / "index.md")))
for fpath in body_files:
try:
text = Path(fpath).read_text(encoding="utf-8")
except OSError:
continue
body_id, bc = _biosphere_from_frontmatter(text)
if body_id and bc:
override[body_id] = bc
# 2. Bodies needing a value + the inputs for the two-gate default.
# Recompute for ALL bodies every run: biosphere_class has no other authoritative
# writer (unlike axial_tilt/radius, which preserve future column writes) — the
# frontmatter override + two-gate default are its complete, deterministic source, so
# an edited override must re-apply on the next regen.
rows = conn.execute(
"""
SELECT b.body_id, b.inhabited, b.atmosphere, b.hydrosphere, e.economic_base_primary
FROM bodies b
LEFT JOIN system_economy e ON e.system_id = b.system_id
"""
).fetchall()
_LIQUID = ("ocean", "liquid", "rivers", "moderate", "extensive", "coastal", "lakes")
def _default(inhabited, atmosphere, hydrosphere, econ, body_id) -> str:
atm = (atmosphere or "").strip().lower()
hyd = (hydrosphere or "").strip().lower()
# Gate 0 — habitability.
if not inhabited or atm in ("", "none", "trace"):
return "Airless"
if atm == "thin":
return "TerraformedSterile"
if not any(k in hyd for k in _LIQUID):
return "Airless" # real atmosphere, no open liquid water -> sealed/engineered
# Gate 1 — Compatible vs Mirror (Alive).
base = (econ or "").strip().lower()
if "agricultur" in base or "service" in base:
return "NativeCompatible"
if "penal" in base or "extract" in base or "manufactur" in base:
return "NativeMirror"
h = int(hashlib.sha1(str(body_id).encode()).hexdigest(), 16)
return "NativeCompatible" if h % 2 == 0 else "NativeMirror"
updates: list[tuple[str, str]] = []
for body_id, inhabited, atmosphere, hydrosphere, econ in rows:
val = override.get(body_id) or _default(inhabited, atmosphere, hydrosphere, econ, body_id)
updates.append((val, body_id))
if not dry_run and updates:
conn.executemany(
"UPDATE bodies SET biosphere_class = ? WHERE body_id = ?", updates
)
return len(updates)
@@ -320,6 +320,7 @@ COLUMN_MIGRATIONS: list[tuple[str, str, str]] = [
("brand_products", "price_tier", "TEXT"),
("bodies", "body_radius_km", "REAL"), # D-204 — physical radius in km, nullable
("bodies", "axial_tilt_deg", "REAL"), # T-1024, D-239 §2 — axial tilt from body-def frontmatter
("bodies", "biosphere_class", "TEXT"), # D-247, T-1085 — chirality/edibility register from body frontmatter + two-gate default
("meta", "schema_sha", "TEXT"),
("atlas_city_names", "settlement_class", "TEXT"), # D-196 — NULL until placement (#37)
("system_economy", "economic_specialization", "TEXT"), # D-237 — authored specialization layer
+6 -1
View File
@@ -223,10 +223,15 @@ def main() -> None:
print(f" {n_bias} body trait-bias rows")
# 16. axial_tilt_deg from body-def frontmatter (T-1024, D-239 §2)
print(" [16/16] Populating axial_tilt_deg from body-def frontmatter...")
print(" [16/17] Populating axial_tilt_deg from body-def frontmatter...")
n_tilt = bodies.populate_axial_tilt_deg(conn, args.dry_run)
print(f" {n_tilt} bodies updated with axial_tilt_deg")
# 17. biosphere_class from body frontmatter override + two-gate default (D-247, T-1085)
print(" [17/17] Populating biosphere_class (D-247)...")
n_bio = bodies.populate_biosphere_class(conn, args.dry_run)
print(f" {n_bio} bodies updated with biosphere_class")
# Validate structural integrity (FK, chain refs, chain completeness).
# These errors indicate broken imported data — do NOT commit.
print("\n Validating structural integrity...")