Files
settled-reach/tooling/atlas-verify
T
jpmschweitzerandClaude Opus 4.6 690587a894 chore(atlas): add atlas-check diagnostic + handle unusual star_type in verify
atlas-check shows planet count, body summary, and flags for quick
proposal diagnostics. atlas-verify now accepts "unusual" as a valid
star_type alongside "binary".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:11:03 +01:00

153 lines
5.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Verify an atlas proposal JSON against integrity checks.
Usage:
tooling/atlas-verify docs/atlas/proposals/GJ273.json
tooling/atlas-verify docs/atlas/proposals/*.json
"""
import argparse
import json
import sys
from pathlib import Path
def verify_proposal(path: Path) -> list[str]:
"""Return list of error strings. Empty list = pass."""
with open(path) as f:
p = json.load(f)
errors = []
bodies = p.get("bodies", [])
stations = p.get("stations", [])
# 1. Inhabited bodies must have names
for b in bodies:
if b.get("inhabited") and not b.get("proper_name"):
errors.append(f"Inhabited body {b['body_id']} has no proper_name")
# 2. Uninhabited bodies MAY have names (lore reasons) — no check needed
# 3. All stations must have names
for s in stations:
if not s.get("proper_name"):
errors.append(f"Station {s['station_id']} has no proper_name")
# 4. Body count minimum by star type
planets = [b for b in bodies if b["body_type"] == "planet"]
star_type = p.get("spectral_class", "M")
if star_type.startswith(("G", "F")):
min_planets = 8
elif star_type.startswith("K"):
min_planets = 7
else:
min_planets = 6
if len(planets) < min_planets:
errors.append(
f"Only {len(planets)} planets, need {min_planets}+ "
f"for {star_type} star"
)
# 5. Required structures
if not any(b["body_type"] == "oort_cloud" for b in bodies):
errors.append("No oort cloud")
horizons = [s for s in stations if s["station_type"] == "horizon"]
if not horizons:
errors.append("No horizon station")
elif not any(s.get("has_gate_infrastructure") for s in horizons):
errors.append("Horizon station missing has_gate_infrastructure: true")
if not any(b["body_type"] == "asteroid_belt" for b in bodies):
errors.append("No asteroid belt (add one unless wiki contradicts)")
# 6. Orbit consistency — top-level bodies
top_level = [b for b in bodies if not b.get("parent_body_id")]
orbits = [b["orbit_index"] for b in top_level]
if orbits != sorted(orbits):
errors.append(f"Top-level orbit_index not monotonic: {orbits}")
if len(orbits) != len(set(orbits)):
errors.append(f"Duplicate orbit_index in top-level: {orbits}")
# 7. Moon orbit consistency
parents: dict[str, list[int]] = {}
for b in bodies:
pid = b.get("parent_body_id")
if pid:
parents.setdefault(pid, []).append(b["orbit_index"])
for pid, idxs in parents.items():
if idxs != sorted(idxs):
errors.append(f"Moon orbits under {pid} not monotonic: {idxs}")
if len(idxs) != len(set(idxs)):
errors.append(f"Duplicate moon orbit_index under {pid}: {idxs}")
# 8. Parent references valid
body_ids = {b["body_id"] for b in bodies}
for b in bodies:
pid = b.get("parent_body_id")
if pid and pid not in body_ids:
errors.append(
f"Body {b['body_id']} references missing parent {pid}"
)
for s in stations:
oid = s.get("orbits_body_id")
if oid and oid not in body_ids:
errors.append(
f"Station {s['station_id']} references missing body {oid}"
)
# 9. star_type vs spectral_class consistency
star_type_field = p.get("star_type", "")
spectral = p.get("spectral_class", "")
if star_type_field and spectral and star_type_field not in ("binary", "unusual"):
# Strip dwarf/subdwarf prefixes (d, sd) to get actual class letter
s = spectral.lstrip("sd").upper()
spectral_letter = s[0] if s else ""
if spectral_letter and spectral_letter.isalpha():
if star_type_field[0].upper() != spectral_letter:
errors.append(
f"star_type '{star_type_field}' conflicts with "
f"spectral_class '{spectral}'"
)
return errors
def main():
parser = argparse.ArgumentParser(
description="Verify atlas proposal JSON files"
)
parser.add_argument(
"proposals",
nargs="+",
type=Path,
help="Proposal JSON file(s) to verify",
)
args = parser.parse_args()
# Sol is hand-authored with different rules (named uninhabited bodies, etc.)
EXCLUDE = {"GJ0.json", "GJ1221.json"}
total_errors = 0
for path in args.proposals:
if path.name in EXCLUDE:
continue
if not path.exists():
print(f"SKIP — {path} not found")
continue
errors = verify_proposal(path)
if errors:
print(f"FAIL — {path.name}")
for e in errors:
print(f" - {e}")
total_errors += len(errors)
else:
print(f"PASS — {path.name}")
if total_errors:
print(f"\n{total_errors} error(s) across {len(args.proposals)} file(s)")
sys.exit(1)
if __name__ == "__main__":
main()