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>
169 lines
5.7 KiB
Python
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")
|