feat(tooling): scope cross-body dedup to hop+corridor (#833)
Changed dedup key from (corridor, feature_type) to
(hop, corridor, feature_type). Systems at the same gate-hop distance
in the same corridor are near neighbors and shouldn't share feature
names; systems at different hops can. This prevents corpus exhaustion
where Gemma's narrow range-name distribution ("The Ridge", "Blackwood
Range") collides after ~20 bodies and drives fallback rates toward
100%.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1310,7 +1310,7 @@ def name_feature(
|
||||
feature_type: str,
|
||||
ctx: dict,
|
||||
blocklist: set[str],
|
||||
corpus: dict[tuple[str, str], set[str]],
|
||||
corpus: dict[tuple, set[str]],
|
||||
body_used: set[str],
|
||||
stem_counts: dict[str, int],
|
||||
stem_cap: int,
|
||||
@@ -1318,22 +1318,23 @@ def name_feature(
|
||||
world_seed: int,
|
||||
body_id: str,
|
||||
local_id: str,
|
||||
hop: int,
|
||||
log: "Logger",
|
||||
verbose: bool,
|
||||
max_attempts: int = 3,
|
||||
) -> str:
|
||||
"""Request a name from Gemma, enforce blocklist + corridor dedup +
|
||||
"""Request a name from Gemma, enforce blocklist + hop/corridor dedup +
|
||||
per-body cross-type dedup, fall back to the palette generator on
|
||||
persistent failure.
|
||||
|
||||
Dedup scopes:
|
||||
- `corpus[(corridor, feature_type)]` — cross-body dedup within the
|
||||
same corridor and feature type. Two rivers in the north_reach
|
||||
should not share a name; two rivers on opposite arcs can.
|
||||
- `corpus[(hop, corridor, feature_type)]` — cross-body dedup within
|
||||
the same gate-hop distance and corridor. Systems at the same hop
|
||||
in the same corridor are near neighbors and shouldn't share
|
||||
feature names. Systems at different hops can.
|
||||
- `body_used` — per-body set across ALL feature types. Prevents
|
||||
the same name from appearing as a river AND an ocean AND a
|
||||
mountain range on the same world, which reads as ridiculous
|
||||
even when the types differ.
|
||||
mountain range on the same world.
|
||||
"""
|
||||
corridor = ctx.get("cultural_corridor") or "core"
|
||||
if feature_type not in _PROMPT_CONFIG:
|
||||
@@ -1342,7 +1343,7 @@ def name_feature(
|
||||
palette = palette_for(corridor)
|
||||
planet_class = ctx.get("planet_class") or "habitable"
|
||||
|
||||
dedup_key = (corridor, feature_type)
|
||||
dedup_key = (hop, corridor, feature_type)
|
||||
used = corpus.setdefault(dedup_key, set())
|
||||
|
||||
# Capture mode: build the attempt-0 prompt, log it, return a unique
|
||||
@@ -1578,11 +1579,12 @@ def process_body(
|
||||
voice: VoiceSubprocess,
|
||||
conn: sqlite3.Connection,
|
||||
blocklist: set[str],
|
||||
corpus: dict[tuple[str, str], set[str]],
|
||||
corpus: dict[tuple, set[str]],
|
||||
stem_counts: dict[str, int],
|
||||
stem_cap: int,
|
||||
system_hook: str | None,
|
||||
world_seed: int,
|
||||
hop: int,
|
||||
log: "Logger",
|
||||
verbose: bool,
|
||||
) -> dict:
|
||||
@@ -1632,7 +1634,7 @@ def process_body(
|
||||
# Cities
|
||||
for city in markers.get("cities") or []:
|
||||
if not _is_blank(city.get("name")):
|
||||
corpus.setdefault((corridor, _feature_type_for_city(city)), set()).add(
|
||||
corpus.setdefault((hop, corridor, _feature_type_for_city(city)), set()).add(
|
||||
city["name"]
|
||||
)
|
||||
counts["preserved"] += 1
|
||||
@@ -1642,7 +1644,7 @@ def process_body(
|
||||
voice, feature_type, ctx, blocklist, corpus, body_used,
|
||||
stem_counts, stem_cap, system_hook,
|
||||
world_seed, body_id, city.get("id") or "city_?",
|
||||
log, verbose,
|
||||
hop, log, verbose,
|
||||
)
|
||||
city["name"] = name
|
||||
counts["cities"] += 1
|
||||
@@ -1652,14 +1654,14 @@ def process_body(
|
||||
# Rivers
|
||||
for river in markers.get("rivers") or []:
|
||||
if not _is_blank(river.get("name")):
|
||||
corpus.setdefault((corridor, "river"), set()).add(river["name"])
|
||||
corpus.setdefault((hop, corridor, "river"), set()).add(river["name"])
|
||||
counts["preserved"] += 1
|
||||
continue
|
||||
name = name_feature(
|
||||
voice, "river", ctx, blocklist, corpus, body_used,
|
||||
stem_counts, stem_cap, system_hook,
|
||||
world_seed, body_id, river.get("id") or "river_?",
|
||||
log, verbose,
|
||||
hop, log, verbose,
|
||||
)
|
||||
river["name"] = name
|
||||
counts["rivers"] += 1
|
||||
@@ -1669,7 +1671,7 @@ def process_body(
|
||||
# Oceans / seas / lakes
|
||||
for water in markers.get("oceans") or []:
|
||||
if not _is_blank(water.get("name")):
|
||||
corpus.setdefault((corridor, _feature_type_for_ocean(water)), set()).add(
|
||||
corpus.setdefault((hop, corridor, _feature_type_for_ocean(water)), set()).add(
|
||||
water["name"]
|
||||
)
|
||||
counts["preserved"] += 1
|
||||
@@ -1679,7 +1681,7 @@ def process_body(
|
||||
voice, feature_type, ctx, blocklist, corpus, body_used,
|
||||
stem_counts, stem_cap, system_hook,
|
||||
world_seed, body_id, water.get("id") or "water_?",
|
||||
log, verbose,
|
||||
hop, log, verbose,
|
||||
)
|
||||
water["name"] = name
|
||||
counts["oceans"] += 1
|
||||
@@ -1689,7 +1691,7 @@ def process_body(
|
||||
# Mountain ranges
|
||||
for rng_feat in markers.get("mountain_ranges") or []:
|
||||
if not _is_blank(rng_feat.get("name")):
|
||||
corpus.setdefault((corridor, "mountain_range"), set()).add(
|
||||
corpus.setdefault((hop, corridor, "mountain_range"), set()).add(
|
||||
rng_feat["name"]
|
||||
)
|
||||
counts["preserved"] += 1
|
||||
@@ -1698,7 +1700,7 @@ def process_body(
|
||||
voice, "mountain_range", ctx, blocklist, corpus, body_used,
|
||||
stem_counts, stem_cap, system_hook,
|
||||
world_seed, body_id, rng_feat.get("id") or "range_?",
|
||||
log, verbose,
|
||||
hop, log, verbose,
|
||||
)
|
||||
rng_feat["name"] = name
|
||||
counts["mountain_ranges"] += 1
|
||||
@@ -1708,7 +1710,7 @@ def process_body(
|
||||
# POIs
|
||||
for poi in markers.get("pois") or []:
|
||||
if not _is_blank(poi.get("name")):
|
||||
corpus.setdefault((corridor, _feature_type_for_poi(poi)), set()).add(
|
||||
corpus.setdefault((hop, corridor, _feature_type_for_poi(poi)), set()).add(
|
||||
poi["name"]
|
||||
)
|
||||
counts["preserved"] += 1
|
||||
@@ -1718,7 +1720,7 @@ def process_body(
|
||||
voice, feature_type, ctx, blocklist, corpus, body_used,
|
||||
stem_counts, stem_cap, system_hook,
|
||||
world_seed, body_id, poi.get("id") or "poi_?",
|
||||
log, verbose,
|
||||
hop, log, verbose,
|
||||
)
|
||||
poi["name"] = name
|
||||
counts["pois"] += 1
|
||||
@@ -2008,6 +2010,7 @@ def main():
|
||||
|
||||
seen_systems.add(system_id)
|
||||
t0 = time.time()
|
||||
hop = hop_order.get(body_id, (99, ""))[0]
|
||||
counts = process_body(
|
||||
body_id=body_id,
|
||||
system_id=system_id,
|
||||
@@ -2020,6 +2023,7 @@ def main():
|
||||
stem_cap=args.stem_cap,
|
||||
system_hook=system_gttr_hooks.get(system_id),
|
||||
world_seed=args.seed,
|
||||
hop=hop,
|
||||
log=log,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user