fix(tooling): address PR #130 review findings (#833)

Hoshe:
- Mark --dump-prompts / name_feature() as vestigial with TODO note
- Fix --refresh help string: 200 → 1000 (matches actual default)
- Fix _RIVER_POOLS comment numbering: Pool 6 before Pool 5 → correct order
- Remove dead first-pass code in fix_fewshot_bleed.py
- _CAPTURE_FILE leak noted in vestigial TODO

Tyre:
- Fix stale "Gemma 2" strings in banner, argparse description, model help
- Note dead code for cleanup pass (name_feature ~700 lines)

Hoshe (prune):
- prune_atlas_features.py: named features sort before unnamed, preventing
  silent discard of hand-authored names during pruning

naming_core:
- v0.2: few-shot blocklist, stricter is_valid_name (min 3 chars, no digits,
  no brackets), prompt fragment rejection expanded

Miri clarification: the 261 "empty-string" files contain only roads (37)
and railroads (37) — infrastructure features never in naming scope. All
cities/rivers/oceans/mountains/POIs are clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 23:27:44 +02:00
co-authored by Claude Opus 4.6
parent 9aaaf95742
commit 8ba3a1d0a7
3 changed files with 18 additions and 59 deletions
+1 -41
View File
@@ -152,47 +152,7 @@ def main():
print(f"WARNING: only {len(available)} replacements for {len(replacements_needed)} features") print(f"WARNING: only {len(available)} replacements for {len(replacements_needed)} features")
print(" some features will keep their few-shot names") print(" some features will keep their few-shot names")
# Assign replacements deterministically — hash body_id + feature_id # Group by file, reload, replace, write
# to pick from the pool, ensuring each body gets different names
used_per_body = defaultdict(set)
replacement_idx = 0
changed_files = set()
total_replaced = 0
for markers_path, body_id, section, feat in replacements_needed:
old_name = feat["name"]
# Find next available name not yet used on this body
assigned = None
for attempt in range(len(available)):
candidate = available[(replacement_idx + attempt) % len(available)]
if candidate.lower() not in used_per_body[body_id]:
assigned = candidate
replacement_idx = (replacement_idx + attempt + 1) % len(available)
break
if assigned is None:
print(f" SKIP {body_id}/{section}: no unique replacement for \"{old_name}\"")
continue
feat["name"] = assigned
used_per_body[body_id].add(assigned.lower())
global_names.add(assigned.lower())
changed_files.add(markers_path)
total_replaced += 1
# Write changed files
for markers_path in changed_files:
body_id = markers_path.parent.name
m = json.loads(markers_path.read_text())
# Re-apply changes (re-read since we modified feat objects in memory)
# Actually the feat dicts are still referenced — just rewrite
# But we need to reload and re-match since we didn't track which file
# has which changes...
# Simpler approach: reload, replace, write
# Reset and do it properly
replacement_idx = 0 replacement_idx = 0
used_per_body = defaultdict(set) used_per_body = defaultdict(set)
changed_bodies = [] changed_bodies = []
+11 -14
View File
@@ -71,14 +71,11 @@ DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems" WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems"
BLOCKLIST_PATH = TOOLING_DIR / "earth_blocklist.txt" BLOCKLIST_PATH = TOOLING_DIR / "earth_blocklist.txt"
# When --dump-prompts is set, name_feature short-circuits: it builds the # NOTE: --dump-prompts and name_feature() are vestigial from the Gemma 2
# attempt-0 prompt for each feature, writes a JSONL line to this file, # single-name pipeline. The live path uses _batch_fill() →
# and returns a unique deterministic placeholder so the pipeline runs to # name_features_batch() from naming_core.py. The old code is retained
# completion without touching a real LLM. Lets us capture the exact prompt # for reference but not called. TODO(#833): remove in a cleanup pass.
# set any backend (Gemma CPU, Gemma GPU, Haiku) would receive on first _CAPTURE_FILE = None # vestigial — see note above
# attempt — so an offline backend can replay them and the resulting names
# can be A/B compared.
_CAPTURE_FILE = None # set in main() when --dump-prompts is used
# Default binary + model paths. The sr-voice binary is platform-specific # Default binary + model paths. The sr-voice binary is platform-specific
# (GPU backend baked in per-build) and lives OUTSIDE any git worktree so # (GPU backend baked in per-build) and lives OUTSIDE any git worktree so
@@ -562,7 +559,7 @@ _RIVER_POOLS: list[list[tuple[str, str]]] = [
("Italian", "Fiume Marconi"), ("Italian", "Fiume Marconi"),
("Greek", "Petrakis Rema"), ("Greek", "Petrakis Rema"),
], ],
# Pool 6 — founder FIRST name possessive (Clifford's Bay shape) # Pool 5 — founder FIRST name possessive (Clifford's Bay shape)
# Added so first-name-possessive naming joins the rotation alongside # Added so first-name-possessive naming joins the rotation alongside
# the surname pools without replacing any of them. # the surname pools without replacing any of them.
[ [
@@ -573,7 +570,7 @@ _RIVER_POOLS: list[list[tuple[str, str]]] = [
("Portuguese", "Rio de Ana"), ("Portuguese", "Rio de Ana"),
("French", "Rivière d'Elena"), ("French", "Rivière d'Elena"),
], ],
# Pool 5 — classical / institutional / Latinate (occasional ~17%) # Pool 6 — classical / institutional / Latinate (occasional ~17%)
[ [
("British/Australian", "Aqueduct Run"), ("British/Australian", "Aqueduct Run"),
("Italian", "Acqua Vetusta"), ("Italian", "Acqua Vetusta"),
@@ -1973,7 +1970,7 @@ def discover_bodies(
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Batch-name atlas features via Gemma 2 voice pipeline (#833)" description="Batch-name atlas features via Gemma 4 E2B tooling pipeline (#833)"
) )
parser.add_argument("--db", default=str(DB_PATH), help="Path to systems.db") parser.add_argument("--db", default=str(DB_PATH), help="Path to systems.db")
parser.add_argument("--body", help="Process only this body_id") parser.add_argument("--body", help="Process only this body_id")
@@ -1995,7 +1992,7 @@ def main():
parser.add_argument( parser.add_argument(
"--model", "--model",
default=str(DEFAULT_MODEL), default=str(DEFAULT_MODEL),
help="Path to the Gemma 2 GGUF model (ignored in --mock mode)", help="Path to the GGUF model file (ignored in --mock mode)",
) )
parser.add_argument( parser.add_argument(
"--distrobox", "--distrobox",
@@ -2010,7 +2007,7 @@ def main():
"--refresh", "--refresh",
type=int, type=int,
default=1000, default=1000,
help="Restart the voice subprocess every N requests (default: 200) " help="Restart the voice subprocess every N requests (default: 1000) "
"to prevent KV-cache context bleed", "to prevent KV-cache context bleed",
) )
parser.add_argument( parser.add_argument(
@@ -2120,7 +2117,7 @@ def main():
last_hop = hop_order.get(_body_id_from_path(markers_paths[-1])[0], (99, ""))[0] last_hop = hop_order.get(_body_id_from_path(markers_paths[-1])[0], (99, ""))[0]
log.raw("") log.raw("")
log.raw(f" Gemma 2 Batch Naming Pipeline (#833)") log.raw(f" Gemma 4 Batch Naming Pipeline (#833)")
log.raw(f" DB: {db_path}") log.raw(f" DB: {db_path}")
log.raw(f" Mode: {'MOCK' if args.mock else 'REAL'}") log.raw(f" Mode: {'MOCK' if args.mock else 'REAL'}")
log.raw(f" sr-voice: {MOCK_STDIO if args.mock else sr_voice_bin}") log.raw(f" sr-voice: {MOCK_STDIO if args.mock else sr_voice_bin}")
+6 -4
View File
@@ -62,10 +62,11 @@ def prune_mountains(markers: dict, cap: int) -> int:
mtns = markers.get("mountain_ranges") or [] mtns = markers.get("mountain_ranges") or []
if len(mtns) <= cap: if len(mtns) <= cap:
return 0 return 0
# Named features sort first (preserve hand-authored names),
# then by area_cells descending.
ranked = sorted( ranked = sorted(
mtns, mtns,
key=lambda m: int(m.get("area_cells") or 0), key=lambda m: (0 if m.get("name") else 1, -(int(m.get("area_cells") or 0))),
reverse=True,
) )
markers["mountain_ranges"] = ranked[:cap] markers["mountain_ranges"] = ranked[:cap]
return len(mtns) - cap return len(mtns) - cap
@@ -75,10 +76,11 @@ def prune_rivers(markers: dict, cap: int) -> int:
rivers = markers.get("rivers") or [] rivers = markers.get("rivers") or []
if len(rivers) <= cap: if len(rivers) <= cap:
return 0 return 0
# Named features sort first (preserve hand-authored names),
# then by path length descending.
ranked = sorted( ranked = sorted(
rivers, rivers,
key=lambda r: len(r.get("path") or []), key=lambda r: (0 if r.get("name") else 1, -len(r.get("path") or [])),
reverse=True,
) )
markers["rivers"] = ranked[:cap] markers["rivers"] = ranked[:cap]
return len(rivers) - cap return len(rivers) - cap