feat(simulation): D-239 three-carrier foundation (T-1023/1024/1026)
First foundation slice of the Atlas-to-tile derivation model (epic T-974), building the carrier layer ahead of its T-1027+ consumers. T-1026 — Anti-squaring domain warp (D-239 §4): stateless pure fn(seed,body_id,pos)->(f64,f64), ±8m, f64 to the final voxel then as-i32 truncation for IEEE-754 cross-target determinism. New domain_warp.rs, SeedDomain::DomainWarp; golden-vector + cross-thread tests. Position math only — D-010 integer discipline preserved downstream. Marked dead_code until the T-1028 VoxelColumn pipeline consumes it. T-1023 — RegionProfile carrier (D-239 §1,§10): new RegionProfile + TectonicClass/GlaciationGrade/PrecipitationClass enums + BodyParams; derived per-region river_threshold replacing the global 200 for tile consumers. regions: BTreeMap on BodyWorldState, populated via the cascade's new RegionProfile layer (runs when body_params is Some, else falls back to Settlement). D-010 integer discipline, BTree ordering. T-1024 — District climate primitives (D-239 §2): nullable temperature_c + moisture on RegionProfile, mean-annual scalar (no clock dep; dynamic branch deferred to Q-105). Hybrid inputs — new bodies.axial_tilt_deg column imported from planet-gen body-defs (populate_axial_tilt_deg, 2611 bodies), luminosity and orbital distance derived at runtime; greenhouse + diurnal-swing tables in source-canonical climate_constants.toml. D-239 implementation note added. cargo test: 1498 passed, 0 failed. clippy clean (pre-existing large_enum_variant only). make check-systems-db: stamp fresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -480,6 +480,7 @@ COLUMN_MIGRATIONS = [
|
||||
("corporations", "shadow_economy_access", "INTEGER DEFAULT 0"),
|
||||
("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
|
||||
("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
|
||||
@@ -1279,6 +1280,121 @@ def populate_body_radius_km(conn: sqlite3.Connection, dry_run: bool) -> int:
|
||||
return len(updates)
|
||||
|
||||
|
||||
def populate_axial_tilt_deg(conn: sqlite3.Connection, dry_run: bool) -> int:
|
||||
"""Populate bodies.axial_tilt_deg from planet-gen body-def frontmatter (T-1024).
|
||||
|
||||
Reads wiki/star-systems/*/bodies/*/index.md YAML frontmatter and extracts
|
||||
``orbit.axial_tilt_deg``. Only updates rows where axial_tilt_deg IS NULL
|
||||
(preserves any future authoritative column writes).
|
||||
|
||||
Source: body_definition_parser.py writes ``orbit.axial_tilt_deg`` into each
|
||||
body's index.md during the planet-gen batch run. This function mirrors
|
||||
``populate_body_radius_km`` in structure.
|
||||
"""
|
||||
import yaml # stdlib-compatible subset via PyYAML if available, else manual parse
|
||||
|
||||
def _parse_frontmatter_yaml(text: str) -> dict:
|
||||
"""Extract YAML frontmatter block from a markdown file."""
|
||||
lines = text.split("\n")
|
||||
if not lines or lines[0].strip() != "---":
|
||||
return {}
|
||||
end_idx = None
|
||||
for i, line in enumerate(lines[1:], 1):
|
||||
if line.strip() == "---":
|
||||
end_idx = i
|
||||
break
|
||||
if end_idx is None:
|
||||
return {}
|
||||
fm_text = "\n".join(lines[1:end_idx])
|
||||
try:
|
||||
result = yaml.safe_load(fm_text)
|
||||
return result if isinstance(result, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
# Check if yaml is available; if not, use manual extraction.
|
||||
try:
|
||||
import yaml as _yaml_check # noqa: F401
|
||||
has_yaml = True
|
||||
except ImportError:
|
||||
has_yaml = False
|
||||
|
||||
if not has_yaml:
|
||||
# Fallback: manual extraction of axial_tilt_deg from YAML frontmatter.
|
||||
# Scans for " axial_tilt_deg: <value>" under an "orbit:" block.
|
||||
def _parse_frontmatter_manual(text: str) -> dict:
|
||||
lines = text.split("\n")
|
||||
if not lines or lines[0].strip() != "---":
|
||||
return {}
|
||||
in_orbit = False
|
||||
result: dict = {}
|
||||
for line in lines[1:]:
|
||||
if line.strip() == "---":
|
||||
break
|
||||
stripped = line.strip()
|
||||
if stripped == "orbit:":
|
||||
in_orbit = True
|
||||
continue
|
||||
if in_orbit:
|
||||
# Detect leaving the orbit block (non-indented key).
|
||||
if line and not line.startswith(" ") and not line.startswith("\t"):
|
||||
in_orbit = False
|
||||
elif stripped.startswith("axial_tilt_deg:"):
|
||||
_, _, val = stripped.partition(":")
|
||||
try:
|
||||
result["axial_tilt_deg"] = float(val.strip())
|
||||
except ValueError:
|
||||
pass
|
||||
return result
|
||||
|
||||
def _parse_frontmatter_yaml(text: str) -> dict: # type: ignore[misc]
|
||||
return _parse_frontmatter_manual(text)
|
||||
|
||||
# Find all body index.md files.
|
||||
body_dir_pattern = WIKI_STAR_SYSTEMS / "*" / "bodies" / "*" / "index.md"
|
||||
import glob as _glob
|
||||
body_files = sorted(_glob.glob(str(body_dir_pattern)))
|
||||
|
||||
# Build body_id → axial_tilt_deg mapping from frontmatter.
|
||||
tilt_map: dict[str, float] = {}
|
||||
for fpath in body_files:
|
||||
try:
|
||||
text = Path(fpath).read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
continue
|
||||
fm = _parse_frontmatter_yaml(text)
|
||||
if not isinstance(fm, dict):
|
||||
continue
|
||||
body_id = fm.get("id")
|
||||
orbit = fm.get("orbit", {})
|
||||
if isinstance(orbit, dict):
|
||||
tilt = orbit.get("axial_tilt_deg")
|
||||
else:
|
||||
tilt = None
|
||||
if body_id and tilt is not None:
|
||||
try:
|
||||
tilt_map[str(body_id)] = float(tilt)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
# Get all bodies where axial_tilt_deg IS NULL and body_id is in tilt_map.
|
||||
rows = conn.execute(
|
||||
"SELECT body_id FROM bodies WHERE axial_tilt_deg IS NULL"
|
||||
).fetchall()
|
||||
|
||||
updates = []
|
||||
for (body_id,) in rows:
|
||||
if body_id in tilt_map:
|
||||
updates.append((tilt_map[body_id], body_id))
|
||||
|
||||
if not dry_run and updates:
|
||||
conn.executemany(
|
||||
"UPDATE bodies SET axial_tilt_deg = ? WHERE body_id = ?", updates
|
||||
)
|
||||
|
||||
return len(updates)
|
||||
|
||||
|
||||
# Atlas geometry index tables (D-191). These hold computed positions — city
|
||||
# centres, road/river/rail polylines, ocean/mountain extents. Under D-223 the
|
||||
# Python atlas geometry generator was retired (#951); the deterministic
|
||||
@@ -2414,6 +2530,11 @@ def main():
|
||||
n_bias = populate_atlas_body_trait_bias(conn, args.dry_run)
|
||||
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...")
|
||||
n_tilt = populate_axial_tilt_deg(conn, args.dry_run)
|
||||
print(f" {n_tilt} bodies updated with axial_tilt_deg")
|
||||
|
||||
# Validate structural integrity (FK, chain refs, chain completeness).
|
||||
# These errors indicate broken imported data — do NOT commit.
|
||||
print("\n Validating structural integrity...")
|
||||
|
||||
Reference in New Issue
Block a user