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>
94 lines
3.2 KiB
Python
94 lines
3.2 KiB
Python
"""Logic for the `pr` domain. Transport-agnostic (D-263)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from tooling.core import config, process
|
|
from tooling.core.errors import ReachError
|
|
|
|
LOGIN = "schweitz"
|
|
REPO = "jpmschweitzer/settled-reach"
|
|
|
|
# Paths whose change means the committed systems.db may be stale. The generator
|
|
# set is read from the registry rather than restated, so this list cannot drift
|
|
# from tooling/generator_sources.py (T-1067).
|
|
EXTRA_WATCHED = (
|
|
"tooling/domains/atlas/planet/import_heightmaps.py",
|
|
"tooling/domains/atlas/planet/import_province_boundaries.py",
|
|
"server/data/systems-schema.sql",
|
|
"wiki/star-systems/",
|
|
"wiki/economics/",
|
|
)
|
|
|
|
|
|
def comment(number: str, body_or_path: str) -> None:
|
|
"""Post a comment on a Gitea PR or issue.
|
|
|
|
`@path` reads the body from a file — the form that exists because a long
|
|
comment inline would need a `$(...)` subshell, and a subshell breaks the
|
|
permission gate's prefix matching (.claude/rules/tea-cli.md).
|
|
|
|
Every flag is explicit for the same reason the rule gives: omitting
|
|
`--login` or `--repo` sends `tea` interactive, and an interactive prompt
|
|
with no TTY does not wait — it crashes.
|
|
"""
|
|
body = _resolve_body(body_or_path)
|
|
|
|
tea = shutil.which("tea")
|
|
if tea is None:
|
|
raise ReachError(
|
|
"the tea CLI is not on PATH",
|
|
fix="tea is symlinked into ~/.local/bin — check that, rather than "
|
|
"calling the linuxbrew path directly, which breaks the Bash(tea *) rule",
|
|
)
|
|
|
|
process.run(
|
|
[tea, "comment", "--login", LOGIN, "--repo", REPO, number, body],
|
|
cwd=config.repo_root(),
|
|
fix=f"check that PR/issue {number} exists and the login is authorised",
|
|
)
|
|
|
|
|
|
def watchlist_diff(base: str, head: str) -> list[str]:
|
|
"""Files changed in the range that would make the committed systems.db stale."""
|
|
from tooling import generator_sources
|
|
|
|
root = config.repo_root()
|
|
registered = sorted(
|
|
{
|
|
str(path.relative_to(root))
|
|
for paths in generator_sources.GENERATOR_SOURCES.values()
|
|
for path in paths
|
|
}
|
|
)
|
|
watched = [*registered, *EXTRA_WATCHED]
|
|
if not watched:
|
|
# The original checked this too: an empty registry would diff nothing
|
|
# and report a clean watchlist, which is the answer that lets a stale DB
|
|
# through.
|
|
raise ReachError(
|
|
"the generator-source registry is empty",
|
|
fix="tooling/generator_sources.py should list every generator input",
|
|
)
|
|
|
|
result = process.run(
|
|
["git", "diff", "--name-only", f"{base}...{head}", "--", *watched],
|
|
cwd=config.repo_root(),
|
|
check=False,
|
|
)
|
|
return [line for line in (result.stdout or "").splitlines() if line]
|
|
|
|
|
|
def _resolve_body(body_or_path: str) -> str:
|
|
if not body_or_path.startswith("@"):
|
|
return body_or_path
|
|
path = Path(body_or_path[1:])
|
|
if not path.is_file():
|
|
raise ReachError(
|
|
f"comment body file not found: {path}",
|
|
fix="write the comment to a file first, then pass @that-path",
|
|
)
|
|
return path.read_text(encoding="utf-8")
|