feat(content): Sol body name pass and atlas tooling — complete #849 atlas cohesion

Finalizes #849 core-world atlas cohesion: GJ0d (Earth/Sol) markers.json
cleaned of erroneous data, refine_log updated with Sol body gap notes,
atlas_quality_analysis.py added for ongoing metric tracking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 14:13:41 +02:00
co-authored by Claude Sonnet 4.6
parent 8f807c56f0
commit 6d50096571
4 changed files with 585 additions and 365 deletions
@@ -0,0 +1,248 @@
#!/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:
python3 tooling/planet-gen/atlas_quality_analysis.py [--db server/data/systems.db]
python3 tooling/planet-gen/atlas_quality_analysis.py --system GJ380
python3 tooling/planet-gen/atlas_quality_analysis.py --top-collisions 20
python3 tooling/planet-gen/atlas_quality_analysis.py --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 pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
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}
print("=" * 70)
print("ATLAS QUALITY ANALYSIS — The Settled Reach (#849/#838)")
print(f"DB: {args.db}")
if args.system:
print(f"Filter: system={args.system}")
if args.body:
print(f"Filter: body={args.body}")
print("=" * 70)
# --- 1. Cross-body collisions ---
print("\n[ 1. CROSS-BODY NAME COLLISIONS ]")
collisions = cross_body_collisions(conn, limit=args.top_collisions)
for feat_type, rows in collisions.items():
if rows:
print(f"\n {feat_type}:")
for name, cnt, bodies in rows:
print(f" '{name}' — {cnt} bodies: {bodies[:80]}")
# --- 2. Per-body quality scores ---
print("\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"])
print(f"\n {'Body':<28} {'System':<12} {'Corridor':<15} "
f"{'Coll%':>6} {'Card%':>6} {'Gen':>4} {'Echo':>4}")
for bid, info, rep in reports[:30]:
print(
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 ---
print("\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):
print(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]
print(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 ""
print(f" {feat_type:<10} {local_id:<12} {name}{flag}{cardinal}{generic}")
# --- 5. Sol gap check ---
print("\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"
print(f" {bid:<15} {bname or '?':<20} pop={pop or '?'} {status}")
conn.close()
print("\nDone.")
def main():
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()
run_analysis(args)
if __name__ == "__main__":
main()
+82 -14
View File
@@ -234,7 +234,37 @@ in log and move on if already above quality bar."
#### GJ0d — Earth (8.5B pop, core world)
50 cities from earth_features.json (London, Tokyo, Delhi, Shanghai, etc.). sol_import.py
sol_import.py originally placed 50 cities from earth_features.json. Team-lead directed trim
to 8-12 cultural touchstones (max 8 cities on any other body; 50 = 13% of all atlas cities
on one body). Criterion: would a player setting a bookmark to "Earth" recognize this as a
touchstone? One per major historical/cultural cluster.
**Cities kept (11):**
| City | Cluster |
|---|---|
| London | Western Europe — historical capital |
| Moscow | Eastern Europe / Russia |
| Istanbul | Bridge city — Europe-Asia hinge |
| New York | North America |
| São Paulo | South America |
| Cairo | Africa + ancient world |
| Delhi | South Asia |
| Tokyo | Japan / East Asia |
| Beijing | China / East Asia |
| Singapore | Southeast Asia — maritime hub |
| Sydney | Oceania |
**Cities cut (39):** Paris, Berlin, Mexico City, Los Angeles, Toronto, Chicago, Lima, Bogotá,
Rio de Janeiro, Buenos Aires, Lagos, Kinshasa, Johannesburg, Nairobi, Tehran, Baghdad, Riyadh,
Ankara, Karachi, Shanghai, Mumbai, Jakarta, Dhaka, Manila, Bangkok, Seoul, Osaka, Chongqing,
Kolkata, Lahore, Shenzhen, Bangalore, Ho Chi Minh City, Luanda, Addis Ababa, Santiago, Taipei,
Hong Kong, Casablanca.
**Generator note:** sol_import.py has no `--top-n` city filter — it uses the full earth_features.json
list. If Sol is regenerated, earth_features.json should be trimmed to the 11 kept cities, or a
filter added in sol_import.py. Filed as finding in #853.
auto-detected 11 rivers and 1 ocean. Three rivers were auto-detected ocean-channel artifacts
in the western Pacific island region; named with geographically proximate rivers. The large
ocean (area_fraction=0.7049) represents Earth's interconnected world ocean.
@@ -246,7 +276,7 @@ ocean (area_fraction=0.7049) represents Earth's interconnected world ocean.
| river | null (river_4, ~27°N/136°E) | Tone River | Japan, Kanto plain |
| river | null (river_10, ~23°N/135°E) | Cagayan | Northern Philippines, largest Philippine river |
**DB sync:** `generate_atlas.py --body GJ0d` ✓
**DB sync:** `generate_atlas.py --body GJ0d` ✓ (11 cities, trimmed from 50)
---
@@ -316,20 +346,58 @@ Team-lead confirmed: defer this sprint. Finding documented for future pass.
---
## Audit metrics (before/after comparison)
## Audit metrics — final confirmed delta
All audited systems post-fix:
Final audit run: 2026-04-19. All 6 systems re-run after all edits. Results below are
from `atlas_cohesion_audit.py` against `server/data/systems.db`.
| System | Empty names before | After | Lazy outputs before | After | Cardinals before | After | Key issues resolved |
|--------|-------------------|----|--------------------|----|-----------------|-------|-----|
| GJ 144 (Ran) | 0 | 0 | 10+ | 0 | 1 | 0 | Aldren/GJ380c cross-system collision fixed |
| GJ 71 (Tau Ceti) | 0 | 0 | 2 | 0 | 0 | 0 | Concordia, Basilica Nova cross-body collisions |
| GJ 244A (Sirius) | 0 | 0 | 2 | 0 | 3 | 0 | Westwall not present; Cairnside resolved |
| GJ 380 (Groombridge) | 0 | 0 | 2 | 0 | 0 | 0 | Lazy suffixes on two #833 features |
| GJ 699 (Barnard's) | 0 | 0 | 1 | 0 | 0 | 0 | Civic vocab on rivers/oceans; street addresses on moon |
| GJ 0 (Sol) | 33 | 0 | 0 | 0 | 0 | 0 | All auto-detected null-name features named |
### Inhabited body targets (in scope for this ticket)
**Total name edits across all bodies:** 23 (non-Sol) + 33 (Sol) = 56 total.
| System | Body | Empty names: before→after | Lazy outputs: before→after | Cardinals: before→after |
|--------|------|--------------------------|---------------------------|------------------------|
| GJ 144 | GJ144d Kallast (2B) | 0→0 | 3→0 | 0→0 |
| GJ 144 | GJ144e Vethis (1.2B) | 0→0 | 7→0 | 1→0 |
| GJ 71 | GJ71c Threshold (600M) | 0→0 | 1→0 | 0→0 |
| GJ 71 | GJ71d Arden (500M) | 0→0 | 1→0 | 0→0 |
| GJ 71 | GJ71d-1 Verantis (20M) | 0→0 | 0→0 | 0→0 |
| GJ 244A | GJ244Ad Edict (400M) | 0→0 | 2→0 | 3→0 |
| GJ 380 | GJ380c Lendel (900M) | 0→0 | 2→0 | 0→0 |
| GJ 699 | GJ699b Verada (1.9B) | 0→0 | 8→0 | 0→0 |
| GJ 699 | GJ699b-1 (uninhabited moon) | 8→0 | 8→0 | 0→0 |
| GJ 0 | GJ0d Earth (8.5B) | 4→0 | 0→0 | 0→0 |
| GJ 0 | GJ0d-1 Luna (350M) | 24→0 | 0→0 | 0→0 |
| GJ 0 | GJ0e Mars (1.2B) | 4→0 | 0→0 | 0→0 |
| GJ 0 | GJ0f-2 Europa (30M) | 1→0 | 0→0 | 0→0 |
**All inhabited targets: zero empty names, zero lazy outputs, zero cardinals after fixes.**
### Remaining audit flags — out of scope or false positives
After fixes, the audit still reports flags on:
**Out of scope — uninhabited/low-pop bodies (not "high-visibility"):**
- GJ144b, GJ144c, GJ144d-1, GJ144e-1, GJ144f, GJ144g-1, GJ144g-2: "Canyon View", "Dry Gulch",
"Stone Creek" etc. These are #833 batch artifacts on non-target bodies. Captured in #853.
- GJ71e: "Meridian Point" (uninhabited body, not in scope)
- GJ380b, GJ380d, GJ380e: various lazy patterns (uninhabited, not in scope)
**False positives on quality cross-reference names (distinctive stem + common suffix):**
- `'Rán's Run'` (GJ144d) — Rán- arc; % Run pattern-matched but stem is unique proper name
- `'Greywash Fork'` (GJ144e) — Grey- arc; % Fork but Greywash is not a generic stem
- `'Kelside Run'` (GJ144e) — Kel- arc; % Run but Kelside is distinctive
- `'Greystone Ridge'` (GJ144e) — Grey- arc; % Ridge but Greystone is distinctive
These four are intentional renames (listed in the FIXES table above) that happen to end with
a suffix in LAZY_PATTERNS. The script does not evaluate stem quality, only suffix pattern.
A future pass on the audit script could add a stem-distinctiveness filter.
### Earth city count correction
Per team-lead direction: Earth trimmed from **50 → 11 cities** (cultural/historical touchstones,
one per major cluster). 39 cities cut. DB synced. See GJ0d section above for full cut list.
**Total name edits across all bodies:** 23 (non-Sol) + 33 (Sol) = 56 feature renames.
**Earth city cut:** 39 removed.
Intentional same-body cross-feature stem dups (quality arcs) now visible in audit output for:
- GJ144d: Rán- (city + river), Seter- (mountain + ocean)
@@ -378,7 +446,7 @@ SR_DB_PATH="$(pwd)/server/data/systems.db" tooling/db/sqlite-query \
- `wiki/star-systems/GJ-699/bodies/GJ699b/markers.json` (8 edits)
- `wiki/star-systems/GJ-699/bodies/GJ699b-1/markers.json` (8 edits)
- `tooling/planet-gen/sol_name_fixes.py` (new — names 33 null-name Sol features)
- `wiki/star-systems/GJ-0/bodies/GJ0d/markers.json` (1 ocean + 3 rivers named)
- `wiki/star-systems/GJ-0/bodies/GJ0d/markers.json` (1 ocean + 3 rivers named; 39 cities cut → 11)
- `wiki/star-systems/GJ-0/bodies/GJ0d-1/markers.json` (24 mountain ranges named)
- `wiki/star-systems/GJ-0/bodies/GJ0e/markers.json` (4 mountain ranges named)
- `wiki/star-systems/GJ-0/bodies/GJ0f-2/markers.json` (1 mountain range named)