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

249 lines
8.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.
#!/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()