Files
jpmschweitzerandClaude Opus 5 b1b57d603f feat(config): T-1285 — the atlas authoring verbs, and a verify nobody checked
reach atlas db / names / systems-done / check / verify / commit-and-sync /
update-field / flatness. Eight scripts retired, five of them bash. verify
reproduces the original exactly: 2 errors across 301 proposals, exit 1.

The binary has more verbs than its wrapper documented. The bash usage text
listed four; atlas-commit-and-sync calls four more it never mentioned. All
eight are declared so reach atlas --help is a complete index, and unknown verbs
are still forwarded — a hand-maintained list falls behind the binary it
describes, so rejecting on it would break the day someone adds a subcommand.

commit-and-sync now stages by default and commits only with --commit. Nothing
else in reach writes to git history, and committing as a side effect of "sync"
is a different risk class from writing a file; the default prints the message
it would use, leaving the decision where it was.

Two real bugs found in that script while porting it. It ran atlas-verify and
never checked the exit code, so a proposal that FAILED verification was still
wiped, committed and synced — bad data in systems.db is far harder to undo than
a failed command, and it now refuses. And it hardcoded a pinned
"Co-Authored-By: Claude Opus 4.6" into every atlas commit, which the git-commit
skill names as the root cause of attribution drift.

update-field gains two guards the original lacked. Its field→table map lived
inside a bash heredoc string where nothing could check it, and an unknown field
produced an UPDATE against a table of None; it now names the nine accepted
fields. And it checks rowcount, so a system_id that does not exist is a failure
rather than a silent no-op reported as success.

The three Python scripts moved with the usual treatment — prints to console
events, argparse replaced by typed functions, __file__ roots to
config.repo_root(). No root bug this time: checked before moving rather than
after, three domains running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 14:36:51 +02:00

139 lines
4.9 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 json
from pathlib import Path
from tooling.core import console
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
# Sol is hand-authored with different rules — named uninhabited bodies and so
# on — so verifying it against the generated-proposal schema would report
# failures for things that are deliberate.
EXCLUDE = {"GJ0.json", "GJ1221.json"}
def verify_all(proposals: list[Path]) -> int:
"""Verify proposal JSONs. Returns the total error count across all files."""
total = 0
for path in proposals:
if path.name in EXCLUDE:
continue
if not path.exists():
console.event(f"SKIP — {path} not found", level="warn")
continue
errors = verify_proposal(path)
if errors:
console.event(f"FAIL — {path.name}", level="error")
for error in errors:
console.event(f" - {error}", level="error")
total += len(errors)
else:
console.event(f"PASS — {path.name}")
return total