"""Logic for the `atlas` domain. Transport-agnostic (D-263). Ported from four bash scripts. Each of them shelled out to the Rust binary and then piped the JSON through a `python3 -c` heredoc — so the logic was already Python, just unreachable: not importable, not testable, and invisible to anything that indexes the tree. """ from __future__ import annotations import json import sqlite3 from pathlib import Path from tooling.core import config, console, process from tooling.core.errors import ReachError, unknown_choice from tooling.domains.atlas import binary, verify # Which table owns which field. The old script carried this map inside a heredoc # string, where nothing could check it against the schema. FIELD_TABLE = { "star_type": "star_systems", "spectral_class": "star_systems", "proper_name": "star_systems", "geographic_sector": "star_systems", "habitable_planet_count": "star_systems", "inhabited_planet_count": "star_systems", "gate_topology": "system_gates", "aperture_count": "system_gates", "hop_distance_from_gateway": "system_gates", } def proper_names() -> list[str]: """Every proper name in the atlas, for collision avoidance when authoring.""" names = set() for verb, key in (("list-bodies", "proper_name"), ("list-stations", "proper_name")): for record in json.loads(binary.run(verb) or "[]"): if record.get(key): names.add(record[key]) return sorted(names) def systems_with_bodies() -> list[str]: """System ids that already have bodies — i.e. already authored.""" bodies = json.loads(binary.run("list-bodies") or "[]") return sorted({body["system_id"] for body in bodies}) def update_field(system_id: str, field: str, value: str) -> str: """Amend one field on a star system. Returns the table it was written to. **Not the asset-pipeline violation it resembles.** That rule forbids raw SQL against systems.db because the next `make regen-db` silently reverts it — and `import_economics` touches only `currency_zone` and `gate_energy_connected` on `star_systems`, not these columns. These come from the one-time baked imports the same rule describes separately, so the edits persist and there is no regen path to route them through. This is the sanctioned way to amend baked atlas data. The real caveat is different: a value amended here lives only in a committed binary, so it cannot be regenerated and will not show in a diff. """ table = FIELD_TABLE.get(field) if table is None: raise unknown_choice("field", field, FIELD_TABLE) database = config.path("server", "data", "systems.db") if not database.is_file(): raise ReachError( f"systems.db not found at {database}", fix="make regen-db", ) connection = sqlite3.connect(str(database)) try: cursor = connection.execute( f"UPDATE {table} SET {field} = ? WHERE system_id = ?", # noqa: S608 (value, system_id), ) if cursor.rowcount == 0: raise ReachError( f"no {table} row for system {system_id!r}", fix="reach atlas systems-done — to see which systems exist", ) connection.commit() finally: connection.close() return table def commit_and_sync(system_id: str, corridor: str, commit: bool) -> None: """Verify a proposal, load it into the DB, sync the wiki, stage the result. **Staging is the default; committing needs `--commit`.** The original always committed. Nothing else in reach writes to git history, and a tool that commits on your behalf as a side effect of "sync" is a different risk class from one that writes a file — so the default reports what it would commit and leaves the decision where it was. """ slug = system_id.replace(" ", "") proposal = config.path("docs", "atlas", "proposals", f"{slug}.json") if not proposal.is_file(): raise ReachError( f"proposal not found: {proposal}", fix=f"author docs/atlas/proposals/{slug}.json first", ) console.event(f"verifying {proposal.name}", phase="atlas") if verify.verify_all([proposal]): raise ReachError( f"{proposal.name} did not verify — not committing it to the DB", fix="fix the errors above; a proposal that fails verification would " "put bad data in systems.db, which is far harder to undo", ) data = json.loads(proposal.read_text(encoding="utf-8")) proper = data.get("proper_name") or data["system_id"] existing = json.loads(binary.run("show-system", system_id) or "{}") if existing.get("bodies"): console.event(f"wiping existing data for {system_id}", phase="atlas", level="warn") binary.run("wipe-system", system_id) console.event(f"committing {system_id} to systems.db", phase="atlas") binary.run("commit-system", str(proposal)) console.event("syncing wiki", phase="atlas") binary.run("sync-wiki", system_id) wiki = config.path("wiki", "star-systems", system_id.replace(" ", "-"), "index.md") staged = [proposal] + ([wiki] if wiki.is_file() else []) for path in staged: process.run(["git", "add", str(path)], cwd=config.repo_root()) message = f"data(atlas): author {system_id} ({proper}) — {corridor}" if not commit: console.verdict( f"staged {len(staged)} file(s) for {system_id} ({proper}) — NOT committed\n" f" would commit as: {message}\n" " pass --commit to make the commit" ) return process.run(["git", "commit", "-m", message], cwd=config.repo_root()) console.verdict(f"committed {system_id} ({proper}) — {corridor}") def proposal_paths(names: list[str]) -> list[Path]: """Resolve proposal arguments, defaulting to the whole directory.""" if names: return [Path(name) for name in names] return sorted(config.path("docs", "atlas", "proposals").glob("*.json"))