feat(tooling): per-system sub-style rotation across all corridors (#833)

Replace single-inflection corridor palettes with lists of sub-styles.
Each system picks one deterministically via hash(system_id), so all
bodies in the same system share a cultural register but neighbouring
systems get different registers.

Core corridor splits into 6 sub-styles (English rural, British
colonial, US rural, US cosmopolitan, classical/institutional,
Australian/NZ). North/south/east/west reach each get 5 sub-styles
covering their cultural spectrum. Deep frontier gets 3 (founder-name,
surveyor-descriptive, outpost-functional).

This multiplies Gemma's effective vocabulary per corridor by the
sub-style count, dramatically reducing dedup pressure. A 6-style
core corridor means each sub-style serves ~4 systems instead of 24,
so "The Ridge" exhausts after ~4 systems, not ~24.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 08:53:02 +02:00
co-authored by Claude Opus 4.6
parent 3ab28138a0
commit cb178a8c57
+103 -72
View File
@@ -176,71 +176,95 @@ class Logger:
# `star_systems.geographic_sector` directly — that column is the real
# source of corridor identity in systems.db (cultural_corridor is a
# legacy field that was never populated beyond sol-gateway-axis).
CORRIDOR_PALETTES: dict[str, dict[str, str]] = {
CORRIDOR_SUBSTYLES: dict[str, list[dict[str, str]]] = {
# Each corridor has a list of sub-style inflections. The pipeline
# picks one per body via hash(body_id) so neighbouring bodies on the
# same planet get different registers, and Gemma's narrow per-register
# vocabulary (~15 stems) stays fresh across hundreds of bodies.
#
# IMPORTANT: the inflection is a DOMINANT bias, not a hard lock.
# Earlier iterations used single-culture labels like
# "administrative English" or "Korean/Japanese/Taiwanese" which
# Gemma 2 2B interpreted as "produce ONLY in this register" — and
# every core body came out anglophone, every east_reach body came
# out East Asian, etc. The real Earth diaspora in the setting is
# cosmopolitan: a British surveyor on an east_reach moon still
# names a river after their aunt in Dorset. The inflections now
# explicitly name the dominant register AND invite diaspora
# variety so Gemma samples from the full cross-cultural few-shot
# pool instead of collapsing to the dominant label.
"core": {
"inflection": "Gateway-era Earth diaspora, mostly English, "
"all Earth cultures welcome",
"examples": "Meridian, Concord, East Ridge, Landing, Tanaka, "
"Rahman, Okafor, Ribeiro",
},
"north_reach": {
"inflection": "British/Australian/Irish dominant, all Earth "
"cultures welcome",
"examples": "Wolcott, Mildern, Ashbourne, Kiln, Briarfell, "
"Tarndale, Ribeiro, Nakamura, Kowalski",
},
"south_reach": {
"inflection": "Portuguese/Swahili/Cape Verdean/Brazilian "
"dominant, all Earth cultures welcome",
"examples": "Vargas, Inhaca, Monteforte, Serra, Ribeiro, "
"Kilimi, Holmberg, Fairview, Tanaka",
},
"east_reach": {
"inflection": "Korean/Japanese/Taiwanese dominant, all Earth "
"cultures welcome",
"examples": "Hanyang, Takamine, Seoraksan, Tsukuri, Ginoza, "
"Baektu, Kellogg, Oliveira, Novak",
},
"west_reach": {
"inflection": "German/Dutch/Nordic dominant, all Earth cultures "
"welcome",
"examples": "Vanebach, Kloosterdam, Bergfjord, Hellekade, "
"Straend, Cooper, Vargas, Nakamura",
},
"deep_frontier": {
"inflection": "frontier founder-name era, any Earth culture",
"examples": "Okafor Reach, Stenner Cross, Weller Hold, Pruitt "
"Basin, Vickery Hold, Kellogg Run, Nakamura Post, "
"Ribeiro Landing",
},
# Legacy keys retained for backward compatibility with the
# cultural_corridor column on the one system that uses it.
"sol-gateway-axis": {
"inflection": "Gateway-era Earth diaspora, mostly English, "
"all Earth cultures welcome",
"examples": "Meridian, Concord, Cardinal, Prefecture, Tanaka, "
"Rahman, Okafor",
},
"inner_corridor": {
"inflection": "Gateway-era Earth diaspora, mostly English, "
"all Earth cultures welcome",
"examples": "Meridian, Concord, East Ridge, Landing, Tanaka, "
"Rahman, Okafor",
},
# A British surveyor on an east_reach moon still names a river after
# their aunt in Dorset. Each sub-style explicitly names its register
# AND invites diaspora variety.
"core": [
{"inflection": "English countryside, rural, agricultural settlers",
"examples": "Thornbury, Bramblewood, Millbrook, Ashford, Weston"},
{"inflection": "British colonial settlement era",
"examples": "New Bristol, Port Augusta, Kingstown, Admiralty, Georgetown"},
{"inflection": "American frontier, practical, geographic",
"examples": "Dusty Creek, Twin Oaks, Cedar Flat, Hawk's Hollow, Red Bluff"},
{"inflection": "American municipal, administrative, cosmopolitan",
"examples": "Prospect Heights, Liberty, Union, Meridian, Commonwealth"},
{"inflection": "Classical references, institutional, civic",
"examples": "Concordia, Aurelius, Prefecture, Senate Landing, Forum"},
{"inflection": "Australian and New Zealand settler",
"examples": "Redfern, Glenelg, Wollongong, Kaikoura, Hawke's Bay"},
],
"north_reach": [
{"inflection": "English rural, village and parish names",
"examples": "Wolcott, Mildern, Ashbourne, Briarfell, Tarndale"},
{"inflection": "Scottish Highland and Lowland place-names",
"examples": "Glenmoray, Dunfermline, Kinross, Brae, Dalwhinnie"},
{"inflection": "Australian outback, station and property names",
"examples": "Redfern, Birdsville, Tennant, Woomera, Coober"},
{"inflection": "Irish rural and coastal settlement",
"examples": "Ballymore, Kilrush, Dunmore, Tralee, Skellig"},
{"inflection": "South African English settler",
"examples": "Grahamstown, Oudtshoorn, Stellenbosch, Graaff, Beaufort"},
],
"south_reach": [
{"inflection": "Portuguese colonial era, Iberian",
"examples": "Monteforte, Serra, Tavira, Oliveira, Porto Novo"},
{"inflection": "Brazilian interior, frontier settlement",
"examples": "Ribeirão, Campo Largo, Várzea, Ilhabela, Pinheiro"},
{"inflection": "East African Swahili coastal",
"examples": "Inhambane, Kilimi, Ngola, Manhica, Quelimane"},
{"inflection": "Cape Verdean and West African",
"examples": "Cabo, Moçambo, Ribeira, Mindelo, Tarrafal"},
{"inflection": "Angolan and Mozambican settlement",
"examples": "Huambo, Lobito, Nampula, Lichinga, Benguela"},
],
"east_reach": [
{"inflection": "Korean place-name tradition",
"examples": "Hanyang, Seorak, Baektu, Saeyeon, Taegong"},
{"inflection": "Japanese rural and coastal settlement",
"examples": "Takamine, Ginoza, Tsukuri, Aomori, Fukagawa"},
{"inflection": "Taiwanese and Hakka settler",
"examples": "Jiufen, Beigang, Hsinchu, Meinong, Tainan"},
{"inflection": "Filipino settler community",
"examples": "Batangas, Legazpi, Tuguegarao, Zambales, Tarlac"},
{"inflection": "Mixed East Asian diaspora, cosmopolitan",
"examples": "Naruhan, Morimine, Kōzan, Midori, Kawasaki"},
],
"west_reach": [
{"inflection": "German settlement, orderly and compound names",
"examples": "Altdorf, Drachenberg, Feldberg, Krakenberg, Lüneborg"},
{"inflection": "Dutch colonial, low-country",
"examples": "Kloosterdam, Hoogland, Oudewater, Nieuwpoort, Voorhout"},
{"inflection": "Nordic and Scandinavian",
"examples": "Sørholm, Torsfell, Bergfjord, Lindeborg, Nordhölm"},
{"inflection": "Polish and Czech settler",
"examples": "Krakowice, Bystrica, Wieliczka, Tarnów, Ostrava"},
{"inflection": "Baltic and Finnish settler",
"examples": "Järvenpää, Tallinna, Pärnu, Turku, Rakvere"},
],
"deep_frontier": [
{"inflection": "frontier founder-name era, surname-first, any Earth culture",
"examples": "Okafor Reach, Stenner Cross, Weller Hold, Pruitt Basin"},
{"inflection": "frontier descriptive, geographic features named by surveyors",
"examples": "Red Mesa, Dry Fork, Iron Flat, Long Ridge, Dust Basin"},
{"inflection": "frontier outpost, functional and military",
"examples": "Forward Post, Relay Station, Survey Camp, Waypoint, Anchor"},
],
}
DEFAULT_PALETTE = CORRIDOR_PALETTES["core"]
# Legacy aliases
CORRIDOR_SUBSTYLES["sol-gateway-axis"] = CORRIDOR_SUBSTYLES["core"]
CORRIDOR_SUBSTYLES["inner_corridor"] = CORRIDOR_SUBSTYLES["core"]
CORRIDOR_SUBSTYLES["inner_orbit"] = CORRIDOR_SUBSTYLES["core"]
CORRIDOR_SUBSTYLES["frontier"] = CORRIDOR_SUBSTYLES["deep_frontier"]
DEFAULT_SUBSTYLES = CORRIDOR_SUBSTYLES["core"]
# Processing order for the main run — core first so those bodies win
# the dedup race and the outer sectors fall into the palette fallback
@@ -255,10 +279,16 @@ SECTOR_PRIORITY: dict[str, int] = {
}
def palette_for(corridor: str | None) -> dict[str, str]:
if not corridor:
return DEFAULT_PALETTE
return CORRIDOR_PALETTES.get(corridor, DEFAULT_PALETTE)
def palette_for(corridor: str | None, system_id: str = "") -> dict[str, str]:
"""Pick a sub-style for this system within its corridor.
All bodies in the same system get the same sub-style (consistent
cultural register per star system). Different systems rotate through
the sub-style list via hash(system_id).
"""
substyles = CORRIDOR_SUBSTYLES.get(corridor or "core", DEFAULT_SUBSTYLES)
idx = int(hashlib.sha256(system_id.encode()).hexdigest()[:8], 16) % len(substyles)
return substyles[idx]
# ---------------------------------------------------------------------------
@@ -1315,6 +1345,7 @@ def name_feature(
stem_counts: dict[str, int],
stem_cap: int,
system_hook: str | None,
system_id: str,
world_seed: int,
body_id: str,
local_id: str,
@@ -1340,7 +1371,7 @@ def name_feature(
if feature_type not in _PROMPT_CONFIG:
return fallback_name(corridor, feature_type, _seed_for(world_seed, body_id, local_id, 0))
palette = palette_for(corridor)
palette = palette_for(corridor, system_id)
planet_class = ctx.get("planet_class") or "habitable"
dedup_key = (hop, corridor, feature_type)
@@ -1633,7 +1664,7 @@ def process_body(
feature_type = _feature_type_for_city(city)
name = name_feature(
voice, feature_type, ctx, blocklist, corpus, body_used,
stem_counts, stem_cap, system_hook,
stem_counts, stem_cap, system_hook, system_id,
world_seed, body_id, city.get("id") or "city_?",
hop, log, verbose,
)
@@ -1651,7 +1682,7 @@ def process_body(
continue
name = name_feature(
voice, "river", ctx, blocklist, corpus, body_used,
stem_counts, stem_cap, system_hook,
stem_counts, stem_cap, system_hook, system_id,
world_seed, body_id, river.get("id") or "river_?",
hop, log, verbose,
)
@@ -1672,7 +1703,7 @@ def process_body(
feature_type = _feature_type_for_ocean(water)
name = name_feature(
voice, feature_type, ctx, blocklist, corpus, body_used,
stem_counts, stem_cap, system_hook,
stem_counts, stem_cap, system_hook, system_id,
world_seed, body_id, water.get("id") or "water_?",
hop, log, verbose,
)
@@ -1692,7 +1723,7 @@ def process_body(
continue
name = name_feature(
voice, "mountain_range", ctx, blocklist, corpus, body_used,
stem_counts, stem_cap, system_hook,
stem_counts, stem_cap, system_hook, system_id,
world_seed, body_id, rng_feat.get("id") or "range_?",
hop, log, verbose,
)
@@ -1713,7 +1744,7 @@ def process_body(
feature_type = _feature_type_for_poi(poi)
name = name_feature(
voice, feature_type, ctx, blocklist, corpus, body_used,
stem_counts, stem_cap, system_hook,
stem_counts, stem_cap, system_hook, system_id,
world_seed, body_id, poi.get("id") or "poi_?",
hop, log, verbose,
)