#!/usr/bin/env python3 """QA report on atlas naming quality and distribution. Runs checks against systems.db and markers.json files: - Exact duplicates within systems - Stem repetition (shared root words) - Prompt fragment leaks - Register bleed (wrong cultural register for corridor) - Feature-type mismatches (street names as mountains, etc.) - Short/long name outliers - Body-name echo (planet name used as feature stem) - Coverage gaps - Distribution by corridor and register Usage: python3 tooling/planet-gen/qa_naming.py python3 tooling/planet-gen/qa_naming.py --verbose """ import re import sqlite3 import sys from collections import Counter, defaultdict from pathlib import Path TOOLING_DIR = Path(__file__).resolve().parent REPO_ROOT = (TOOLING_DIR / ".." / "..").resolve() DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems" sys.path.insert(0, str(TOOLING_DIR)) from naming_core import _words # --------------------------------------------------------------------------- # Data loading # --------------------------------------------------------------------------- def load_all_names(conn): """Load all named features grouped by system and body.""" results = [] for table, ftype in [ ("atlas_cities", "city"), ("atlas_rivers", "river"), ("atlas_mountain_ranges", "mountain"), ("atlas_oceans", "ocean"), ("atlas_pois", "poi"), ]: rows = conn.execute(f""" SELECT a.body_id, a.name, b.system_id, COALESCE(s.geographic_sector, 'unknown') as corridor, COALESCE(s.proper_name, s.system_id) as system_name, COALESCE(b.proper_name, '') as body_name, b.inhabited FROM {table} a JOIN bodies b ON a.body_id = b.body_id JOIN star_systems s ON b.system_id = s.system_id WHERE a.name IS NOT NULL AND a.name != '' """).fetchall() for body_id, name, system_id, corridor, sys_name, body_name, inhabited in rows: results.append({ "body_id": body_id, "name": name, "system_id": system_id, "corridor": corridor, "system_name": sys_name, "body_name": body_name, "feature_type": ftype, "inhabited": bool(inhabited), }) return results # --------------------------------------------------------------------------- # Checks # --------------------------------------------------------------------------- def check_prompt_fragments(names): """Find names that look like prompt leaks.""" fragments = [ "style:", "answer:", "insert your", "example", "placeholder", "number only", "names:", "generate", "already used", "do not", "must be", "distinct", "comma-separated", "best:", "option", ] hits = [] for n in names: low = n["name"].lower() for frag in fragments: if frag in low: hits.append((n["name"], n["body_id"], n["system_name"], frag)) break return hits def check_exact_dupes_within_system(names): """Find exact duplicate names within the same system + feature type.""" by_sys_type = defaultdict(list) for n in names: key = (n["system_id"], n["feature_type"]) by_sys_type[key].append(n) dupes = [] for key, group in by_sys_type.items(): seen = {} for n in group: low = n["name"].lower() if low in seen: dupes.append((n["name"], n["body_id"], seen[low], n["system_name"], n["feature_type"])) else: seen[low] = n["body_id"] return dupes def check_exact_dupes_within_body(names): """Find exact duplicate names within the same body across all types.""" by_body = defaultdict(list) for n in names: by_body[n["body_id"]].append(n) dupes = [] for body_id, group in by_body.items(): seen = {} for n in group: low = n["name"].lower() if low in seen: dupes.append((n["name"], body_id, n["feature_type"], seen[low], n["system_name"])) else: seen[low] = n["feature_type"] return dupes def check_body_name_echo(names): """Find names where the body/system proper name dominates.""" hits = [] for n in names: if not n["body_name"]: continue body_stem = n["body_name"].lower() name_low = n["name"].lower() # Check if body name appears as a word in the feature name name_words = set(name_low.split()) body_words = set(body_stem.split()) if body_words & name_words: hits.append((n["name"], n["body_name"], n["body_id"], n["system_name"])) return hits def check_register_bleed(names): """Find names that look like they're from the wrong cultural register. Uses keyword heuristics — not perfect but catches obvious mismatches. """ # Register keywords that should NOT appear in certain corridors bleed_patterns = { "core": { "wrong": ["kimchi", "samurai", "fjord", "veld", "kopje", "baobab"], }, "east_reach": { "wrong": ["bramble", "meadow", "thatch", "croft", "basilica", "forum", "senate"], }, "west_reach": { "wrong": ["sakura", "bamboo", "lotus", "jade", "dragon", "phoenix"], }, "south_reach": { "wrong": ["fjord", "viking", "norse", "highland", "glen"], }, "north_reach": { "wrong": ["sakura", "bamboo", "jade", "polder", "graben"], }, } # NZ/Australian names in non-core/north corridors nz_keywords = ["pōhutukawa", "waitara", "wairarapa", "fiordland", "aotearoa", "rangitoto", "wellington", "canterbury", "auckland", "otago", "kauri", "pukekohe", "taranaki", "moana"] hits = [] for n in names: low = n["name"].lower() corridor = n["corridor"] # Check NZ bleed into non-Australian registers if corridor not in ("core", "north_reach"): for kw in nz_keywords: if kw in low: hits.append((n["name"], n["body_id"], corridor, n["system_name"], f"NZ/AU keyword '{kw}' in {corridor}")) break # Check corridor-specific wrong keywords if corridor in bleed_patterns: for kw in bleed_patterns[corridor]["wrong"]: if kw in low: hits.append((n["name"], n["body_id"], corridor, n["system_name"], f"keyword '{kw}' wrong for {corridor}")) break return hits def check_feature_type_mismatch(names): """Find names that sound wrong for their feature type.""" # Street/road names shouldn't be mountains street_words = {"street", "avenue", "boulevard", "drive", "road", "lane", "way", "highway", "route", "thoroughfare"} # Building names shouldn't be rivers/oceans building_words = {"hall", "house", "building", "tower", "plaza", "square", "station", "terminal", "center", "centre"} hits = [] for n in names: words = set(n["name"].lower().split()) if n["feature_type"] == "mountain" and words & street_words: hits.append((n["name"], n["body_id"], n["feature_type"], n["system_name"], f"street name as mountain")) if n["feature_type"] in ("river", "ocean") and words & building_words: # Allow "Hall" for classical register if n["corridor"] != "core": hits.append((n["name"], n["body_id"], n["feature_type"], n["system_name"], f"building name as {n['feature_type']}")) return hits def check_stem_repetition(names): """Find bodies where too many features share the same first word.""" by_body = defaultdict(list) for n in names: by_body[n["body_id"]].append(n) hits = [] for body_id, group in by_body.items(): # Count first significant word per feature type by_type = defaultdict(list) for n in group: by_type[n["feature_type"]].append(n["name"]) for ftype, fnames in by_type.items(): if len(fnames) < 4: continue first_words = [_words(name)[0] if _words(name) else "" for name in fnames] counts = Counter(first_words) for word, count in counts.most_common(3): if count >= 4 and word: hits.append((body_id, ftype, word, count, len(fnames), group[0]["system_name"])) return hits def check_short_long_names(names): """Find very short (1 word, ≤3 chars) or very long names.""" short = [(n["name"], n["body_id"], n["system_name"]) for n in names if len(n["name"]) <= 3] long_ = [(n["name"], n["body_id"], n["system_name"]) for n in names if len(n["name"]) > 40] return short, long_ def check_numbers_in_names(names): """Find names containing digits.""" return [(n["name"], n["body_id"], n["system_name"]) for n in names if re.search(r"\d", n["name"])] def corridor_distribution(names): """Count names per corridor.""" counts = Counter(n["corridor"] for n in names) return counts def feature_type_distribution(names): """Count names per feature type.""" counts = Counter(n["feature_type"] for n in names) return counts def coverage_gaps(conn): """Find bodies with unnamed features.""" gaps = [] for table, ftype in [ ("atlas_mountain_ranges", "mountain"), ("atlas_oceans", "ocean"), ("atlas_pois", "poi"), ]: rows = conn.execute(f""" SELECT a.body_id, COUNT(*) as total, SUM(CASE WHEN a.name IS NULL OR a.name = '' THEN 1 ELSE 0 END) as blank, COALESCE(s.proper_name, b.system_id) as sys_name, b.inhabited FROM {table} a JOIN bodies b ON a.body_id = b.body_id JOIN star_systems s ON b.system_id = s.system_id GROUP BY a.body_id HAVING blank > 0 ORDER BY b.inhabited DESC, blank DESC """).fetchall() for body_id, total, blank, sys_name, inhabited in rows: gaps.append((body_id, ftype, blank, total, sys_name, bool(inhabited))) return gaps def most_common_names(names, top_n=20): """Find the most frequently used names across all systems.""" counts = Counter(n["name"].lower() for n in names) return counts.most_common(top_n) # --------------------------------------------------------------------------- # Report # --------------------------------------------------------------------------- def main(): verbose = "--verbose" in sys.argv conn = sqlite3.connect(str(DB_PATH), timeout=30.0) print("Loading named features from DB...") names = load_all_names(conn) print(f" {len(names):,} named features loaded\n") # === Distribution === print("=" * 70) print("DISTRIBUTION") print("=" * 70) print("\nBy corridor:") for corridor, count in sorted(corridor_distribution(names).items(), key=lambda x: -x[1]): print(f" {corridor:20s} {count:>6,}") print("\nBy feature type:") for ftype, count in sorted(feature_type_distribution(names).items(), key=lambda x: -x[1]): print(f" {ftype:20s} {count:>6,}") # === Most common names === print(f"\n{'=' * 70}") print("MOST COMMON NAMES (potential over-generation)") print("=" * 70) for name, count in most_common_names(names, 30): if count >= 3: print(f" {count:>4}x {name}") # === Prompt fragments === print(f"\n{'=' * 70}") print("PROMPT FRAGMENT LEAKS") print("=" * 70) fragments = check_prompt_fragments(names) if fragments: for name, body, sys_name, frag in fragments[:20]: print(f" [{sys_name}/{body}] \"{name}\" (matched: {frag})") if len(fragments) > 20: print(f" ... and {len(fragments) - 20} more") else: print(" None found ✓") print(f" Total: {len(fragments)}") # === Exact dupes within system === print(f"\n{'=' * 70}") print("EXACT DUPLICATES WITHIN SYSTEM (same name, same feature type)") print("=" * 70) sys_dupes = check_exact_dupes_within_system(names) if sys_dupes: for name, body1, body2, sys_name, ftype in sys_dupes[:20]: print(f" [{sys_name}] \"{name}\" ({ftype}) on {body1} and {body2}") if len(sys_dupes) > 20: print(f" ... and {len(sys_dupes) - 20} more") else: print(" None found ✓") print(f" Total: {len(sys_dupes)}") # === Exact dupes within body === print(f"\n{'=' * 70}") print("EXACT DUPLICATES WITHIN BODY (same name, different feature types)") print("=" * 70) body_dupes = check_exact_dupes_within_body(names) if body_dupes: for name, body, ftype1, ftype2, sys_name in body_dupes[:20]: print(f" [{sys_name}/{body}] \"{name}\" as {ftype1} and {ftype2}") if len(body_dupes) > 20: print(f" ... and {len(body_dupes) - 20} more") else: print(" None found ✓") print(f" Total: {len(body_dupes)}") # === Stem repetition === print(f"\n{'=' * 70}") print("STEM REPETITION (4+ features sharing first word on same body)") print("=" * 70) stems = check_stem_repetition(names) if stems: for body, ftype, word, count, total, sys_name in stems[:20]: print(f" [{sys_name}/{body}] \"{word}\" appears {count}/{total} times in {ftype}s") if len(stems) > 20: print(f" ... and {len(stems) - 20} more") else: print(" None found ✓") print(f" Total: {len(stems)}") # === Body name echo === print(f"\n{'=' * 70}") print("BODY NAME ECHO (planet name appears in feature name)") print("=" * 70) echoes = check_body_name_echo(names) if echoes: # Group by body by_body = defaultdict(list) for name, body_name, body_id, sys_name in echoes: by_body[(body_id, body_name, sys_name)].append(name) for (body_id, body_name, sys_name), echo_names in sorted( by_body.items(), key=lambda x: -len(x[1]) )[:15]: print(f" [{sys_name}/{body_id}] body=\"{body_name}\": {', '.join(echo_names[:5])}" f"{'...' if len(echo_names) > 5 else ''} ({len(echo_names)} total)") else: print(" None found ✓") print(f" Total: {len(echoes)} names across {len(set(e[2] for e in echoes))} bodies") # === Register bleed === print(f"\n{'=' * 70}") print("REGISTER BLEED (cultural mismatch for corridor)") print("=" * 70) bleeds = check_register_bleed(names) if bleeds: for name, body, corridor, sys_name, reason in bleeds[:30]: print(f" [{sys_name}/{body}] \"{name}\" — {reason}") if len(bleeds) > 30: print(f" ... and {len(bleeds) - 30} more") else: print(" None found ✓") print(f" Total: {len(bleeds)}") # === Feature type mismatch === print(f"\n{'=' * 70}") print("FEATURE TYPE MISMATCH (street names as mountains, etc.)") print("=" * 70) mismatches = check_feature_type_mismatch(names) if mismatches: for name, body, ftype, sys_name, reason in mismatches[:20]: print(f" [{sys_name}/{body}] \"{name}\" — {reason}") if len(mismatches) > 20: print(f" ... and {len(mismatches) - 20} more") else: print(" None found ✓") print(f" Total: {len(mismatches)}") # === Short/long names === print(f"\n{'=' * 70}") print("SHORT NAMES (≤3 chars)") print("=" * 70) short, long_ = check_short_long_names(names) if short: for name, body, sys_name in short[:15]: print(f" [{sys_name}/{body}] \"{name}\"") if len(short) > 15: print(f" ... and {len(short) - 15} more") else: print(" None found ✓") print(f" Total: {len(short)}") print(f"\n{'=' * 70}") print("LONG NAMES (>40 chars)") print("=" * 70) if long_: for name, body, sys_name in long_[:15]: print(f" [{sys_name}/{body}] \"{name}\"") else: print(" None found ✓") print(f" Total: {len(long_)}") # === Numbers in names === print(f"\n{'=' * 70}") print("NUMBERS IN NAMES") print("=" * 70) numbered = check_numbers_in_names(names) if numbered: for name, body, sys_name in numbered[:15]: print(f" [{sys_name}/{body}] \"{name}\"") else: print(" None found ✓") print(f" Total: {len(numbered)}") # === Coverage gaps === print(f"\n{'=' * 70}") print("COVERAGE GAPS (bodies with unnamed features)") print("=" * 70) gaps = coverage_gaps(conn) inhabited_gaps = [g for g in gaps if g[5]] uninhabited_gaps = [g for g in gaps if not g[5]] if inhabited_gaps: print(f"\n INHABITED bodies with gaps ({len(inhabited_gaps)}):") for body, ftype, blank, total, sys_name, _ in inhabited_gaps[:10]: print(f" [{sys_name}/{body}] {blank}/{total} {ftype}s unnamed") if uninhabited_gaps: print(f"\n Uninhabited bodies with gaps ({len(uninhabited_gaps)}):") gap_by_type = Counter(g[1] for g in uninhabited_gaps) for ftype, count in gap_by_type.most_common(): total_blank = sum(g[2] for g in uninhabited_gaps if g[1] == ftype) print(f" {ftype}: {count} bodies, {total_blank} unnamed features") # === Summary === print(f"\n{'=' * 70}") print("SUMMARY") print("=" * 70) total = len(names) issues = (len(fragments) + len(sys_dupes) + len(body_dupes) + len(stems) + len(bleeds) + len(mismatches) + len(short) + len(long_) + len(numbered)) print(f" Total named features: {total:>8,}") print(f" Total QA issues found: {issues:>8,}") print(f" Issue rate: {issues/total*100:>7.2f}%") print(f" Prompt fragment leaks: {len(fragments):>8,}") print(f" Exact dupes (system): {len(sys_dupes):>8,}") print(f" Exact dupes (body): {len(body_dupes):>8,}") print(f" Stem repetition: {len(stems):>8,}") print(f" Body name echo: {len(echoes):>8,}") print(f" Register bleed: {len(bleeds):>8,}") print(f" Feature type mismatch: {len(mismatches):>8,}") print(f" Short names: {len(short):>8,}") print(f" Long names: {len(long_):>8,}") print(f" Numbers in names: {len(numbered):>8,}") print(f" Coverage gaps (inhabited):{len(inhabited_gaps):>8,}") print(f" Coverage gaps (uninh.): {len(uninhabited_gaps):>8,}") conn.close() if __name__ == "__main__": main()