"""Transport for the `atlas` domain — args in, delegate, format out.""" from __future__ import annotations from pathlib import Path import typer from tooling.core import cli, console from tooling.core.command import command from tooling.core.errors import ReachError from tooling.domains.atlas import binary, flatness, proposal_check, service, verify app = cli.domain("atlas", "The spatial ladder — authoring, inspection and the DB.") @app.callback() def _domain() -> None: """Keeps `atlas` a group (Typer collapses a single-command app).""" # --- the Rust binary, fronted --------------------------------------------- @app.command("db") @command def db( verb: str = typer.Argument(..., help=f"One of: {', '.join(binary.KNOWN_VERBS)}"), args: list[str] = typer.Argument(None, help="Arguments passed to the binary."), ) -> None: """Query or mutate the atlas DB through the Rust `atlas` binary. A passthrough on purpose. The verbs live in Rust, so listing them here keeps `reach atlas --help` a complete index — but an unrecognised verb is still forwarded rather than rejected, because a list maintained by hand falls behind the binary it describes. """ output = binary.run(verb, *(args or [])) if output.strip(): console.out(output.rstrip()) # --- read-only queries ---------------------------------------------------- @app.command("names") @command def names() -> None: """Every proper name in the atlas — for collision avoidance when authoring.""" for name in service.proper_names(): console.out(name) @app.command("systems-done") @command def systems_done() -> None: """System ids that already have bodies, i.e. have been authored.""" for system in service.systems_with_bodies(): console.out(system) # --- proposal workflow ---------------------------------------------------- @app.command("check") @command def check( proposals: list[Path] = typer.Argument(None, help="Proposal JSON file(s)."), ) -> None: """Quick diagnostic on a proposal — planet count, body summary, flags.""" targets = service.proposal_paths([str(p) for p in (proposals or [])]) if not targets: raise ReachError( "no proposals found", fix="pass a path, or author one under docs/atlas/proposals/", exit_code=2, ) proposal_check.report(targets) @app.command("verify") @command def verify_proposals( proposals: list[Path] = typer.Argument(None, help="Proposal JSON file(s)."), ) -> None: """Verify proposals against the integrity checks.""" targets = service.proposal_paths([str(p) for p in (proposals or [])]) errors = verify.verify_all(targets) if errors: raise ReachError( f"{errors} error(s) across {len(targets)} file(s)", fix="each failure above names the file and the field at fault", ) console.verdict(f"atlas-verify: OK — {len(targets)} proposal(s)") @app.command("commit-and-sync") @command def commit_and_sync( system_id: str = typer.Argument(..., help="System id, e.g. 'GJ 273'."), corridor: str = typer.Option("east_reach", "--corridor", help="Corridor tag."), commit: bool = typer.Option( False, "--commit", help="Also make the git commit. Without this, files are staged and reported.", ), ) -> None: """Verify a proposal, load it into the DB, sync the wiki, stage the result. STAGES by default; `--commit` makes the commit. The original always committed, and nothing else in reach writes to git history — a tool that commits as a side effect of "sync" is a different risk class from one that writes a file. """ service.commit_and_sync(system_id, corridor, commit) # --- capture analysis ----------------------------------------------------- @app.command("update-field") @command def update_field( system_id: str = typer.Argument(..., help="System id, e.g. 'GJ 273'."), field: str = typer.Argument(..., help="Field to amend."), value: str = typer.Argument(..., help="New value."), ) -> None: """Amend one field on a star system record. This writes SQL directly to systems.db, which looks like the thing .claude/rules/asset-pipeline.md forbids — it is not. That rule exists because regen silently reverts hand edits, and `import_economics` does not own these columns; they come from the one-time baked imports, so the edit persists. Caveat worth knowing: the value then lives only in a committed binary, so it cannot be regenerated and will not show in a diff. """ table = service.update_field(system_id, field, value) console.verdict(f"atlas-update-field: {table}.{field} = {value!r} for {system_id}") @app.command("flatness") @command def flatness_report( images: list[Path] = typer.Argument(None, help="Captures to measure."), ladder: bool = typer.Option( False, "--ladder", help="Measure the standard descent ladder in .cache/screenshots/." ), ) -> None: """Measure how much structure an Atlas capture carries, per rung.""" code = flatness.report(list(images or []), ladder) if code != 0: raise ReachError( "some captures are missing", fix="re-run the capture, or check .cache/screenshots/ for the ladder", exit_code=code, ) # --- the rungs, as nested groups ----------------------------------------- # # `map` (the Reach, the top rung) and `planet` (a body, the third) are nested # GROUPS, not domains of their own: the ladder is one subject (D-243), and each # rung is part of it rather than a peer. They are added at import time, but # their module trees load only when one of their verbs runs — the same lazy # contract as the domains themselves. from tooling.domains.atlas.map.router import app as _map_app # noqa: E402 from tooling.domains.atlas.planet.router import app as _planet_app # noqa: E402 app.add_typer(_map_app, name="map") app.add_typer(_planet_app, name="planet")