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>
249 lines
8.9 KiB
Python
249 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
atlas_quality_analysis.py — Atlas content quality audit for Sprint 36 (#849/#838).
|
||
|
||
Queries atlas_* tables in systems.db and reports on:
|
||
1. Cross-body name collisions (same name, same feature type, different bodies)
|
||
2. Cardinal/directional name density per body
|
||
3. Generic/lazy name patterns
|
||
4. Earth-echo concentration in high-visibility systems
|
||
5. Top-stem frequency across all named features
|
||
|
||
Usage:
|
||
reach atlas planet quality [--db server/data/systems.db]
|
||
reach atlas planet quality --system GJ380
|
||
reach atlas planet quality --top-collisions 20
|
||
reach atlas planet quality --body GJ71c
|
||
|
||
D-191 §8: markers.json is pixel-space [row, col] against 512×256.
|
||
Re-run after any hand-refine pass to verify improvements.
|
||
"""
|
||
|
||
import argparse
|
||
import re
|
||
import sqlite3
|
||
from collections import Counter, defaultdict
|
||
|
||
from tooling.core import config, console
|
||
|
||
|
||
REPO_ROOT = config.repo_root()
|
||
DEFAULT_DB = REPO_ROOT / "server" / "data" / "systems.db"
|
||
|
||
CARDINAL_RE = re.compile(
|
||
r"\b(north|south|east|west|eastern|western|northern|southern|"
|
||
r"upper|lower|new|great|old|central|inner|outer|kita|minami|higashi|nishi)\b",
|
||
re.I,
|
||
)
|
||
GENERIC_RE = re.compile(
|
||
r"\b(hilly|sector|zone|district)\b"
|
||
r"|^(great|the great|hilly)\b"
|
||
r"|^(valley floor|ridge line|ridge crest|flat ground|riverbend)$",
|
||
re.I,
|
||
)
|
||
EARTH_ECHO_RE = re.compile(
|
||
r"\b(manchester|london|paris|berlin|tokyo|beijing|new york|sydney|dubai|"
|
||
r"route \d+|sector \d+|block \d+)\b",
|
||
re.I,
|
||
)
|
||
FEATURE_TABLES = [
|
||
("atlas_cities", "city"),
|
||
("atlas_rivers", "river"),
|
||
("atlas_oceans", "ocean"),
|
||
("atlas_mountain_ranges", "mountain"),
|
||
]
|
||
|
||
|
||
def open_db(path: str) -> sqlite3.Connection:
|
||
return sqlite3.connect(path)
|
||
|
||
|
||
def build_body_index(conn: sqlite3.Connection) -> dict:
|
||
c = conn.cursor()
|
||
c.execute(
|
||
"SELECT body_id, system_id, proper_name, cultural_corridor, population "
|
||
"FROM bodies WHERE inhabited=1"
|
||
)
|
||
return {
|
||
r[0]: {"system_id": r[1], "name": r[2], "corridor": r[3], "pop": r[4]}
|
||
for r in c.fetchall()
|
||
}
|
||
|
||
|
||
def gather_all_names(conn: sqlite3.Connection) -> dict[str, list[tuple[str, str, str]]]:
|
||
"""body_id → [(feature_type, name, local_id), ...]"""
|
||
c = conn.cursor()
|
||
result = defaultdict(list)
|
||
for tbl, feat_type in FEATURE_TABLES:
|
||
try:
|
||
c.execute(f"SELECT body_id, name, local_id FROM {tbl} WHERE name IS NOT NULL AND name != ''")
|
||
for body_id, name, local_id in c.fetchall():
|
||
result[body_id].append((feat_type, name, local_id))
|
||
except sqlite3.OperationalError:
|
||
pass
|
||
return result
|
||
|
||
|
||
def cross_body_collisions(conn: sqlite3.Connection, limit: int = 20) -> dict:
|
||
c = conn.cursor()
|
||
collisions = {}
|
||
for tbl, feat_type in FEATURE_TABLES:
|
||
try:
|
||
c.execute(
|
||
f"SELECT name, COUNT(DISTINCT body_id) as cnt, GROUP_CONCAT(DISTINCT body_id) "
|
||
f"FROM {tbl} WHERE name IS NOT NULL AND name != '' "
|
||
f"GROUP BY name HAVING cnt > 1 ORDER BY cnt DESC LIMIT ?",
|
||
(limit,),
|
||
)
|
||
collisions[feat_type] = [(r[0], r[1], r[2]) for r in c.fetchall()]
|
||
except sqlite3.OperationalError:
|
||
collisions[feat_type] = []
|
||
return collisions
|
||
|
||
|
||
def stem_frequency(names: list[str], top_n: int = 30) -> list[tuple[str, int]]:
|
||
stems = Counter()
|
||
for name in names:
|
||
words = name.split()
|
||
if words:
|
||
stems[words[0].lower()] += 1
|
||
return stems.most_common(top_n)
|
||
|
||
|
||
def body_quality_report(body_id: str, names: list[tuple], conn: sqlite3.Connection) -> dict:
|
||
total = len(names)
|
||
if total == 0:
|
||
return {}
|
||
cardinal = sum(1 for _, n, _ in names if CARDINAL_RE.search(n))
|
||
generic = sum(1 for _, n, _ in names if GENERIC_RE.search(n))
|
||
earth = sum(1 for _, n, _ in names if EARTH_ECHO_RE.search(n))
|
||
|
||
c = conn.cursor()
|
||
# collision count: how many of this body's names appear on other bodies (same type)
|
||
colliding = 0
|
||
for feat_type, name, _ in names:
|
||
tbl = [t for t, f in FEATURE_TABLES if f == feat_type][0]
|
||
try:
|
||
c.execute(
|
||
f"SELECT COUNT(DISTINCT body_id) FROM {tbl} WHERE name=? AND body_id!=?",
|
||
(name, body_id),
|
||
)
|
||
others = c.fetchone()[0]
|
||
if others > 0:
|
||
colliding += 1
|
||
except sqlite3.OperationalError:
|
||
pass
|
||
|
||
return {
|
||
"total": total,
|
||
"cardinal": cardinal,
|
||
"cardinal_pct": cardinal / total,
|
||
"generic": generic,
|
||
"earth_echo": earth,
|
||
"colliding": colliding,
|
||
"colliding_pct": colliding / total,
|
||
}
|
||
|
||
|
||
def run_analysis(args):
|
||
conn = open_db(args.db)
|
||
body_index = build_body_index(conn)
|
||
all_names_by_body = gather_all_names(conn)
|
||
|
||
# Filter by system or body if requested
|
||
if args.system:
|
||
body_index = {k: v for k, v in body_index.items() if v["system_id"] == args.system}
|
||
if args.body:
|
||
body_index = {k: v for k, v in body_index.items() if k == args.body}
|
||
|
||
console.out("=" * 70)
|
||
console.out("ATLAS QUALITY ANALYSIS — The Settled Reach (#849/#838)")
|
||
console.out(f"DB: {args.db}")
|
||
if args.system:
|
||
console.out(f"Filter: system={args.system}")
|
||
if args.body:
|
||
console.out(f"Filter: body={args.body}")
|
||
console.out("=" * 70)
|
||
|
||
# --- 1. Cross-body collisions ---
|
||
console.out("\n[ 1. CROSS-BODY NAME COLLISIONS ]")
|
||
collisions = cross_body_collisions(conn, limit=args.top_collisions)
|
||
for feat_type, rows in collisions.items():
|
||
if rows:
|
||
console.out(f"\n {feat_type}:")
|
||
for name, cnt, bodies in rows:
|
||
console.out(f" '{name}' — {cnt} bodies: {bodies[:80]}")
|
||
|
||
# --- 2. Per-body quality scores ---
|
||
console.out("\n[ 2. BODY QUALITY SCORES — ranked by collision % ]")
|
||
reports = []
|
||
for bid, info in body_index.items():
|
||
names = all_names_by_body.get(bid, [])
|
||
if not names:
|
||
continue
|
||
report = body_quality_report(bid, names, conn)
|
||
if not report:
|
||
continue
|
||
reports.append((bid, info, report))
|
||
|
||
reports.sort(key=lambda x: -x[2]["colliding_pct"])
|
||
|
||
console.out(f"\n {'Body':<28} {'System':<12} {'Corridor':<15} "
|
||
f"{'Coll%':>6} {'Card%':>6} {'Gen':>4} {'Echo':>4}")
|
||
for bid, info, rep in reports[:30]:
|
||
console.out(f" {(info['name'] or bid):<28} {info['system_id']:<12} {info['corridor'] or '?':<15} "
|
||
f"{rep['colliding_pct']:>6.0%} {rep['cardinal_pct']:>6.0%} "
|
||
f"{rep['generic']:>4} {rep['earth_echo']:>4}")
|
||
|
||
# --- 3. Stem frequency ---
|
||
console.out("\n[ 3. TOP STEM FREQUENCY (first word of name) ]")
|
||
all_names_flat = [n for names in all_names_by_body.values() for _, n, _ in names]
|
||
for stem, cnt in stem_frequency(all_names_flat, top_n=20):
|
||
console.out(f" {stem:<20} {cnt}")
|
||
|
||
# --- 4. Detailed body report (if --body specified) ---
|
||
if args.body and args.body in all_names_by_body:
|
||
bid = args.body
|
||
info = body_index.get(bid, {})
|
||
names = all_names_by_body[bid]
|
||
console.out(f"\n[ 4. DETAILED REPORT: {bid} ({info.get('name', '?')}) ]")
|
||
c = conn.cursor()
|
||
for feat_type, name, local_id in sorted(names, key=lambda x: x[0]):
|
||
tbl = [t for t, f in FEATURE_TABLES if f == feat_type][0]
|
||
c.execute(
|
||
f"SELECT COUNT(DISTINCT body_id) FROM {tbl} WHERE name=? AND body_id!=?",
|
||
(name, bid),
|
||
)
|
||
others = c.fetchone()[0]
|
||
flag = f" *** COLLISION ×{others}" if others > 0 else ""
|
||
cardinal = " [cardinal]" if CARDINAL_RE.search(name) else ""
|
||
generic = " [generic]" if GENERIC_RE.search(name) else ""
|
||
console.out(f" {feat_type:<10} {local_id:<12} {name}{flag}{cardinal}{generic}")
|
||
|
||
# --- 5. Sol gap check ---
|
||
console.out("\n[ 5. SOL SYSTEM GAP CHECK ]")
|
||
c = conn.cursor()
|
||
c.execute("SELECT body_id, proper_name, population FROM bodies WHERE system_id='GJ 0' AND inhabited=1")
|
||
sol_bodies = c.fetchall()
|
||
for bid, bname, pop in sol_bodies:
|
||
has_cities = bid in all_names_by_body and any(f == "city" for f, _, _ in all_names_by_body[bid])
|
||
status = "HAS DATA" if has_cities else "*** EMPTY — needs authoring"
|
||
console.out(f" {bid:<15} {bname or '?':<20} pop={pop or '?'} {status}")
|
||
|
||
conn.close()
|
||
console.out("\nDone.")
|
||
|
||
|
||
def main(argv: list[str] | None = None):
|
||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||
parser.add_argument("--db", default=str(DEFAULT_DB), help="Path to systems.db")
|
||
parser.add_argument("--system", help="Filter to one system (e.g. GJ380)")
|
||
parser.add_argument("--body", help="Filter to one body (e.g. GJ71c)")
|
||
parser.add_argument("--top-collisions", type=int, default=15, help="Collision list limit")
|
||
args = parser.parse_args(argv)
|
||
run_analysis(args)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|