Files
settled-reach/tooling/domains/atlas/router.py
T
jpmschweitzerandClaude Opus 5.5 668772075c refactor(tooling): T-1288 — planet-gen becomes reach atlas planet
The 30-file tree moves under atlas as its third rung (D-243), ten verbs
fronting it. Each verb restates its module's options so `--help` describes
something; tooling/test_planet_router.py hands every declared option to the
module's own argparse and fails on drift, and now runs in make test-tooling.

The 2026-09-02 half of this move had converted the top-level imports and the
repo roots. Finishing it found what the half-move left:

- Lazy in-function imports, and all of sol_data/, still named siblings bare.
  They resolved only through sys.path.insert hacks, so under reach the first
  globe render in generate, batch or sol-import would have raised
  ModuleNotFoundError. Qualified; the hacks are gone.
- 247 print() calls and a stdout progress writer that fired once per 8 KB
  block. Report verbs (audit, quality) write through console.out, progress
  through console.event, and download progress is throttled to 10% steps
  so a job log is not tens of thousands of lines.
- Every error exit raises ReachError with a fix.

Two checks that could not fail:

- batch --verify-determinism printed a warning and exited 0 on a mismatch.
- import-provinces exited 0 with errors > 0.

Both now raise. The 271-body bake is only safe to re-run because the first
one holds.

sol-import --body is action="append" in the module but the router took one
value, so --body GJ0d --body GJ0e kept one. Now repeatable, and _flags repeats
list options.

test_conformance walked one level, so a nested group was reported as a verb
missing @command and its ten verbs were never checked. It recurses now;
proven by stripping @command from `planet quality` and watching it fail.

Stray PNGs from the 2026-09-03 runaway router-test run are parked in
.cache/t1288-stray-pngs/, not committed. Their reliefmaps differ from HEAD
while the heightmap regenerated byte-identical — filed as T-1291.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:08:02 +02:00

169 lines
5.7 KiB
Python

"""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 below the body surface -------------------------------------
#
# `planet` is a nested GROUP, not a domain of its own: the ladder is one
# subject (D-243), and a body is a rung of it rather than a peer. It is added
# at import time but its own module tree loads only when a `planet` verb runs
# — same lazy contract as the domains themselves.
from tooling.domains.atlas.planet.router import app as _planet_app # noqa: E402
app.add_typer(_planet_app, name="planet")