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:
2026-06-08 09:28:31 +02:00
co-authored by Claude Opus 4.8
parent b7b4626035
commit def70eaf37
15 changed files with 1659 additions and 6 deletions
+121
View File
@@ -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...")