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