Files
settled-reach/tooling/domains/atlas/router.py
T
jpmschweitzerandClaude Opus 5.5 59b3fc4caa refactor(tooling): T-1293 — atlas map, and the generator that must not run
The star-map family was the last unported part of the tree, and it never had
a ticket. Two of its scripts become `reach atlas map` verbs, nested under
atlas like planet (D-243: the Reach map is the ladder's top rung):

- `reach atlas map data [--check]` regenerates client/data/star_map_data.json.
  The regenerated file differs by one line: `_meta.note`, which named the
  old script's path.
- `reach atlas map svg` renders the concentric SVG (+ PNG), byte-identical
  to the old script's output on the same data.

make check-star-map and star-map-data stay as one-line delegates, because
pre-pr-client and pre-pr-validate depend on check-star-map.

generate-star-map.py, its seed, sculpt-star-map.py and tune-star-map-topology.py
are archived, not ported. The generator rewrites docs/design/star-map.json
unconditionally from an S-NNN-keyed seed, so re-running it would erase the GJ
migration and every hand edit since; sculpt and tune only understand S-NNN
edges. .claude/rules/diagrams.md was telling agents to "edit the generator
and re-run it". It now distinguishes the live concentric render, the seven
frozen S-keyed sector .d2 files (T-1294), and the two SVGs that never had a
generator in the repo.

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

172 lines
5.9 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, 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")