reach atlas db / names / systems-done / check / verify / commit-and-sync / update-field / flatness. Eight scripts retired, five of them bash. verify reproduces the original exactly: 2 errors across 301 proposals, exit 1. The binary has more verbs than its wrapper documented. The bash usage text listed four; atlas-commit-and-sync calls four more it never mentioned. All eight are declared so reach atlas --help is a complete index, and unknown verbs are still forwarded — a hand-maintained list falls behind the binary it describes, so rejecting on it would break the day someone adds a subcommand. commit-and-sync now stages by default and commits only with --commit. Nothing else in reach writes to git history, and committing as a side effect of "sync" is a different risk class from writing a file; the default prints the message it would use, leaving the decision where it was. Two real bugs found in that script while porting it. It ran atlas-verify and never checked the exit code, so a proposal that FAILED verification was still wiped, committed and synced — bad data in systems.db is far harder to undo than a failed command, and it now refuses. And it hardcoded a pinned "Co-Authored-By: Claude Opus 4.6" into every atlas commit, which the git-commit skill names as the root cause of attribution drift. update-field gains two guards the original lacked. Its field→table map lived inside a bash heredoc string where nothing could check it, and an unknown field produced an UPDATE against a table of None; it now names the nine accepted fields. And it checks rowcount, so a system_id that does not exist is a failure rather than a silent no-op reported as success. The three Python scripts moved with the usual treatment — prints to console events, argparse replaced by typed functions, __file__ roots to config.repo_root(). No root bug this time: checked before moving rather than after, three domains running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
154 lines
6.0 KiB
Python
154 lines
6.0 KiB
Python
"""Logic for the `atlas` domain. Transport-agnostic (D-263).
|
|
|
|
Ported from four bash scripts. Each of them shelled out to the Rust binary and
|
|
then piped the JSON through a `python3 -c` heredoc — so the logic was already
|
|
Python, just unreachable: not importable, not testable, and invisible to
|
|
anything that indexes the tree.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
from tooling.core import config, console, process
|
|
from tooling.core.errors import ReachError, unknown_choice
|
|
from tooling.domains.atlas import binary, verify
|
|
|
|
# Which table owns which field. The old script carried this map inside a heredoc
|
|
# string, where nothing could check it against the schema.
|
|
FIELD_TABLE = {
|
|
"star_type": "star_systems",
|
|
"spectral_class": "star_systems",
|
|
"proper_name": "star_systems",
|
|
"geographic_sector": "star_systems",
|
|
"habitable_planet_count": "star_systems",
|
|
"inhabited_planet_count": "star_systems",
|
|
"gate_topology": "system_gates",
|
|
"aperture_count": "system_gates",
|
|
"hop_distance_from_gateway": "system_gates",
|
|
}
|
|
|
|
|
|
def proper_names() -> list[str]:
|
|
"""Every proper name in the atlas, for collision avoidance when authoring."""
|
|
names = set()
|
|
for verb, key in (("list-bodies", "proper_name"), ("list-stations", "proper_name")):
|
|
for record in json.loads(binary.run(verb) or "[]"):
|
|
if record.get(key):
|
|
names.add(record[key])
|
|
return sorted(names)
|
|
|
|
|
|
def systems_with_bodies() -> list[str]:
|
|
"""System ids that already have bodies — i.e. already authored."""
|
|
bodies = json.loads(binary.run("list-bodies") or "[]")
|
|
return sorted({body["system_id"] for body in bodies})
|
|
|
|
|
|
def update_field(system_id: str, field: str, value: str) -> str:
|
|
"""Amend one field on a star system. Returns the table it was written to.
|
|
|
|
**Not the asset-pipeline violation it resembles.** That rule forbids raw SQL
|
|
against systems.db because the next `make regen-db` silently reverts it —
|
|
and `import_economics` touches only `currency_zone` and
|
|
`gate_energy_connected` on `star_systems`, not these columns. These come
|
|
from the one-time baked imports the same rule describes separately, so the
|
|
edits persist and there is no regen path to route them through. This is the
|
|
sanctioned way to amend baked atlas data.
|
|
|
|
The real caveat is different: a value amended here lives only in a committed
|
|
binary, so it cannot be regenerated and will not show in a diff.
|
|
"""
|
|
table = FIELD_TABLE.get(field)
|
|
if table is None:
|
|
raise unknown_choice("field", field, FIELD_TABLE)
|
|
|
|
database = config.path("server", "data", "systems.db")
|
|
if not database.is_file():
|
|
raise ReachError(
|
|
f"systems.db not found at {database}",
|
|
fix="make regen-db",
|
|
)
|
|
|
|
connection = sqlite3.connect(str(database))
|
|
try:
|
|
cursor = connection.execute(
|
|
f"UPDATE {table} SET {field} = ? WHERE system_id = ?", # noqa: S608
|
|
(value, system_id),
|
|
)
|
|
if cursor.rowcount == 0:
|
|
raise ReachError(
|
|
f"no {table} row for system {system_id!r}",
|
|
fix="reach atlas systems-done — to see which systems exist",
|
|
)
|
|
connection.commit()
|
|
finally:
|
|
connection.close()
|
|
return table
|
|
|
|
|
|
def commit_and_sync(system_id: str, corridor: str, commit: bool) -> None:
|
|
"""Verify a proposal, load it into the DB, sync the wiki, stage the result.
|
|
|
|
**Staging is the default; committing needs `--commit`.** The original always
|
|
committed. Nothing else in reach writes to git history, and a tool that
|
|
commits on your behalf as a side effect of "sync" is a different risk class
|
|
from one that writes a file — so the default reports what it would commit
|
|
and leaves the decision where it was.
|
|
"""
|
|
slug = system_id.replace(" ", "")
|
|
proposal = config.path("docs", "atlas", "proposals", f"{slug}.json")
|
|
if not proposal.is_file():
|
|
raise ReachError(
|
|
f"proposal not found: {proposal}",
|
|
fix=f"author docs/atlas/proposals/{slug}.json first",
|
|
)
|
|
|
|
console.event(f"verifying {proposal.name}", phase="atlas")
|
|
if verify.verify_all([proposal]):
|
|
raise ReachError(
|
|
f"{proposal.name} did not verify — not committing it to the DB",
|
|
fix="fix the errors above; a proposal that fails verification would "
|
|
"put bad data in systems.db, which is far harder to undo",
|
|
)
|
|
|
|
data = json.loads(proposal.read_text(encoding="utf-8"))
|
|
proper = data.get("proper_name") or data["system_id"]
|
|
|
|
existing = json.loads(binary.run("show-system", system_id) or "{}")
|
|
if existing.get("bodies"):
|
|
console.event(f"wiping existing data for {system_id}", phase="atlas", level="warn")
|
|
binary.run("wipe-system", system_id)
|
|
|
|
console.event(f"committing {system_id} to systems.db", phase="atlas")
|
|
binary.run("commit-system", str(proposal))
|
|
|
|
console.event("syncing wiki", phase="atlas")
|
|
binary.run("sync-wiki", system_id)
|
|
|
|
wiki = config.path("wiki", "star-systems", system_id.replace(" ", "-"), "index.md")
|
|
staged = [proposal] + ([wiki] if wiki.is_file() else [])
|
|
for path in staged:
|
|
process.run(["git", "add", str(path)], cwd=config.repo_root())
|
|
|
|
message = f"data(atlas): author {system_id} ({proper}) — {corridor}"
|
|
if not commit:
|
|
console.verdict(
|
|
f"staged {len(staged)} file(s) for {system_id} ({proper}) — NOT committed\n"
|
|
f" would commit as: {message}\n"
|
|
" pass --commit to make the commit"
|
|
)
|
|
return
|
|
|
|
process.run(["git", "commit", "-m", message], cwd=config.repo_root())
|
|
console.verdict(f"committed {system_id} ({proper}) — {corridor}")
|
|
|
|
|
|
def proposal_paths(names: list[str]) -> list[Path]:
|
|
"""Resolve proposal arguments, defaulting to the whole directory."""
|
|
if names:
|
|
return [Path(name) for name in names]
|
|
return sorted(config.path("docs", "atlas", "proposals").glob("*.json"))
|