Files
settled-reach/tooling/domains/atlas/planet/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

262 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Transport for `atlas planet` — args in, delegate, format out.
Every verb here declares its options explicitly rather than forwarding an
opaque argument list. That is deliberate and it is the more expensive option:
the underlying modules already parse their own arguments, so this restates ten
surfaces that exist elsewhere.
It is worth it because the primary user of reach is an agent (D-263), and an
agent discovers a command by reading its `--help`. A passthrough would make
`reach atlas planet generate --help` describe nothing, and the real surface
would only be findable by reading the module — which is the fragmentation the
whole CLI exists to end.
The cost is a second place that can drift. `tooling/test_planet_router.py`
closes that: it hands every declared option to the module's own parser and
fails if the parser does not recognise it.
"""
from __future__ import annotations
from pathlib import Path
import typer
from tooling.core import cli
from tooling.core.command import command
app = cli.domain("planet", "Bodies — scaffold, generate, import and audit.")
@app.callback()
def _rung() -> None:
"""Keeps `planet` a group (Typer collapses a single-command app)."""
def _flags(**pairs: object) -> list[str]:
"""Turn declared options into the argv the module's parser expects.
None means "not given" and is dropped, so the module's own defaults stay
authoritative — restating them here would be a second source of truth for
every default in the domain. A list repeats the flag, for the parsers that
declare `action="append"`.
"""
argv: list[str] = []
for name, value in pairs.items():
flag = "--" + name.replace("_", "-")
if value is None or value is False:
continue
if value is True:
argv.append(flag)
elif isinstance(value, (list, tuple)):
for item in value:
argv += [flag, str(item)]
else:
argv += [flag, str(value)]
return argv
# --- generation -----------------------------------------------------------
@app.command("generate")
@command
def generate(
body_def: Path = typer.Argument(..., help="Body definition JSON."),
system: str = typer.Option(None, "--system", help="System id to generate for."),
overrides: Path = typer.Option(None, "--overrides", help="Override JSON."),
output_dir: Path = typer.Option(None, "--output-dir", help="Where to write output."),
heightmap_size: str = typer.Option(
None, "--heightmap-size", help="Heightmap grid as WxH, e.g. 1024x512."
),
globe_size: int = typer.Option(None, "--globe-size", help="Globe render edge px."),
render_mode: str = typer.Option(None, "--render-mode", help="Renderer mode."),
chrome: bool = typer.Option(False, "--chrome", help="Draw chrome on the render."),
) -> None:
"""Generate one body — simulate, render the heightmap, write the globe."""
from tooling.domains.atlas.planet import generate as impl
impl.main([
str(body_def),
*_flags(
system=system,
overrides=overrides,
output_dir=output_dir,
heightmap_size=heightmap_size,
globe_size=globe_size,
render_mode=render_mode,
chrome=chrome,
),
])
@app.command("batch")
@command
def batch(
system: str = typer.Option(None, "--system", help="Limit to one system."),
body: str = typer.Option(None, "--body", help="Limit to one body."),
scaffold_only: bool = typer.Option(False, "--scaffold-only", help="Scaffold, do not generate."),
generate_only: bool = typer.Option(False, "--generate-only", help="Generate, do not scaffold."),
overrides: Path = typer.Option(None, "--overrides", help="Override JSON."),
force: bool = typer.Option(False, "--force", help="Regenerate what already exists."),
dry_run: bool = typer.Option(False, "--dry-run", help="Report the plan, write nothing."),
verify_determinism: int = typer.Option(
None, "--verify-determinism", metavar="N",
help="Generate N random bodies twice and verify the output is identical.",
),
heightmap_size: str = typer.Option(
None, "--heightmap-size", help="Heightmap grid as WxH, e.g. 1024x512."
),
globe_size: int = typer.Option(None, "--globe-size", help="Globe render edge px."),
) -> None:
"""Generate every system unattended — the long one; consider --detach."""
from tooling.domains.atlas.planet import batch as impl
impl.main(_flags(
system=system,
body=body,
scaffold_only=scaffold_only,
generate_only=generate_only,
overrides=overrides,
force=force,
dry_run=dry_run,
verify_determinism=verify_determinism,
heightmap_size=heightmap_size,
globe_size=globe_size,
))
@app.command("scaffold")
@command
def scaffold(
system_index: Path = typer.Argument(..., help="System index JSON."),
overrides: Path = typer.Option(None, "--overrides", help="Override JSON."),
dry_run: bool = typer.Option(False, "--dry-run", help="Report the plan, write nothing."),
) -> None:
"""Write body definitions for a system, ready for `generate`."""
from tooling.domains.atlas.planet import scaffold_bodies as impl
impl.main([str(system_index), *_flags(overrides=overrides, dry_run=dry_run)])
# --- imports into the atlas DB -------------------------------------------
@app.command("import-heightmaps")
@command
def import_heightmaps(
db: Path = typer.Option(None, "--db", help="Atlas DB path."),
body: str = typer.Option(None, "--body", help="Limit to one body."),
limit: int = typer.Option(None, "--limit", help="Stop after N bodies."),
dry_run: bool = typer.Option(False, "--dry-run", help="Report, write nothing."),
skip_rename: bool = typer.Option(False, "--skip-rename", help="Do not rename source files."),
) -> None:
"""Load heightmap grids into atlas_body_heightmaps.
A one-time build import, deliberately NOT part of `make regen-db` and not
stamped (.claude/rules/asset-pipeline.md). Running it is a decision, not a
step in the pipeline.
"""
from tooling.domains.atlas.planet import import_heightmaps as impl
impl.main(_flags(db=db, body=body, limit=limit, dry_run=dry_run, skip_rename=skip_rename))
@app.command("import-provinces")
@command
def import_provinces(
db: Path = typer.Option(None, "--db", help="Atlas DB path."),
body: str = typer.Option(None, "--body", help="Limit to one body."),
force: bool = typer.Option(False, "--force", help="Recompute bodies that already have rows."),
dry_run: bool = typer.Option(False, "--dry-run", help="Report, write nothing."),
verbose: bool = typer.Option(False, "--verbose", help="Per-body detail."),
) -> None:
"""Derive province boundaries from watershed analysis (D-205, D-208).
The other one-time build import — same standing as import-heightmaps.
Expect ~15–20 minutes for a full run; `--detach` and tail it.
"""
from tooling.domains.atlas.planet import import_province_boundaries as impl
impl.main(_flags(db=db, body=body, force=force, dry_run=dry_run, verbose=verbose))
@app.command("terrain-reference")
@command
def terrain_reference(
db: Path = typer.Option(None, "--db", help="Atlas DB path."),
) -> None:
"""Populate the terrain reference table."""
from tooling.domains.atlas.planet import populate_terrain_reference as impl
impl.main(_flags(db=db))
# --- Sol ------------------------------------------------------------------
@app.command("sol-import")
@command
def sol_import(
body: list[str] = typer.Option(None, "--body", help="Limit to these Sol bodies (repeatable)."),
download_only: bool = typer.Option(False, "--download-only", help="Fetch source data only."),
output_dir: Path = typer.Option(None, "--output-dir", help="Where to write output."),
heightmap_size: str = typer.Option(
None, "--heightmap-size", help="Heightmap grid as WxH, e.g. 1024x512."
),
globe_size: int = typer.Option(None, "--globe-size", help="Globe render edge px."),
render_mode: str = typer.Option(None, "--render-mode", help="Renderer mode."),
) -> None:
"""Import real Sol bodies from published elevation data."""
from tooling.domains.atlas.planet import sol_import as impl
impl.main(_flags(
body=body,
download_only=download_only,
output_dir=output_dir,
heightmap_size=heightmap_size,
globe_size=globe_size,
render_mode=render_mode,
))
@app.command("sol-name-fixes")
@command
def sol_name_fixes(
dry_run: bool = typer.Option(False, "--dry-run", help="Report, write nothing."),
) -> None:
"""Name the auto-detected features on Sol bodies that were left unnamed."""
from tooling.domains.atlas.planet import sol_name_fixes as impl
impl.main(_flags(dry_run=dry_run))
# --- audits ---------------------------------------------------------------
@app.command("audit")
@command
def audit(
system: str = typer.Option(None, "--system", help="Limit to one system."),
body: str = typer.Option(None, "--body", help="Limit to one body."),
db: Path = typer.Option(None, "--db", help="Atlas DB path."),
) -> None:
"""Cohesion audit — does the atlas hang together across bodies."""
from tooling.domains.atlas.planet import atlas_cohesion_audit as impl
impl.main(_flags(system=system, body=body, db=db))
@app.command("quality")
@command
def quality(
db: Path = typer.Option(None, "--db", help="Atlas DB path."),
system: str = typer.Option(None, "--system", help="Limit to one system."),
body: str = typer.Option(None, "--body", help="Limit to one body."),
top_collisions: int = typer.Option(None, "--top-collisions", help="How many to list."),
) -> None:
"""Quality analysis — name collisions and distribution."""
from tooling.domains.atlas.planet import atlas_quality_analysis as impl
impl.main(_flags(db=db, system=system, body=body, top_collisions=top_collisions))