feat(tooling): per-system gttr context + cosmopolitan corridor palettes (#833)
Two related quality fixes observed mid-run on Sirius + ACB + Ran:
1) Cosmopolitan corridor palettes. The six corridor inflection labels
were single-culture dominant ("administrative English / Gateway-era",
"British / Australian / Irish", "Korean/Japanese/Taiwanese", etc).
Gemma 2 2B interpreted these as "produce ONLY in this register" and
every core body came out anglophone, every east_reach body came out
East Asian. 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 labels now spell out the dominant register
AND explicitly invite cross-cultural variety so Gemma samples from
the full few-shot pool instead of collapsing to one culture.
2) Per-system gttr context (the big one). The gttr.md files under
wiki/star-systems/<slug>/gttr.md already carry a vivid one-sentence
characterisation of every system — "where the rules live", "forty
years old and still in the draft", "the most connected system in
the Reach", "grandparents owned the land". This is a far stronger
cultural signal than the corridor inflection alone.
New column `star_systems.gttr_hook` stores a pre-extracted 45-word
hook per system. `tooling/db/populate_gttr_hook.py` parses each
gttr.md, regex-matches the first `**NAME**` paragraph, normalises
whitespace, truncates softly at a word cap, and stores it. Covers
all 301 systems (full coverage). Idempotent, safe to re-run after
any wiki update. Explicit transaction wrapper.
gemma_naming.py loads the hook cache at startup via
`load_system_gttr_hooks` and threads `system_hook` plus the system
and body proper names through process_body → name_feature →
_build_prompt. The prompt now carries:
System: <proper_name>. Planet: <body_name>.
About the system: <gttr_hook>
Style: British. Answer: Cooper's Creek
Style: Dutch. Answer: Meijer Beek
...
Real-mode smoke on 10 cases across 4 contrasting systems shows the
hook is doing exactly what it should. Sample output on the same
body_id / local_id pairs:
Tau Ceti (cosmopolitan hub) → Oakham River, Riverwood, Bridle Way
Ran (old-family agricultural) → Hart's Well, Blackwood Ridge
ACB (Lattice Commission seat) → Greenhaven, Rudge Brook
Posto Avançado (PT frontier dead-end) → Rio Preto, Serra de Caxias, Cunha's Cove
Posto Avançado went from "likely-English under the old corridor-only
prompt" to actual Portuguese names with a real Brazilian place stem
(Caxias), because the hook explicitly mentions wave_5 Portuguese
founders and frontier dead-end context. The gttr cultural one-liner
is the single strongest lever available for per-system cohesion —
this was the mono-culture issue observed in the first run, now fixed.
Token cost: ~60-90 extra tokens per prompt (hook + ident line).
Inference slowdown: ~5-10% per call. Acceptable for the quality gain.
Also restores 10 markers.json files that were stale from the aborted
run just killed — they were all core bodies at hop 0-1 which benefit
most from the gttr-context upgrade, so re-running them with the new
prompt is worth the ~3 minutes of re-inference.
This commit is contained in:
@@ -38,6 +38,15 @@ CREATE TABLE IF NOT EXISTS star_systems (
|
|||||||
cultural_corridor TEXT,
|
cultural_corridor TEXT,
|
||||||
generation_priority TEXT,
|
generation_priority TEXT,
|
||||||
|
|
||||||
|
-- Short narrative hook extracted from the wiki's gttr.md file — the
|
||||||
|
-- first characterisation paragraph ("where the rules live", "forty
|
||||||
|
-- years old and still in the draft", etc). Populated by
|
||||||
|
-- tooling/db/populate_gttr_hook.py from wiki/star-systems/<slug>/gttr.md.
|
||||||
|
-- Used as compact cultural context in the Gemma 2 naming pipeline
|
||||||
|
-- (#833) so per-system feel is grounded in the canonical identity
|
||||||
|
-- rather than generic corridor labels.
|
||||||
|
gttr_hook TEXT,
|
||||||
|
|
||||||
-- Economics (D-172)
|
-- Economics (D-172)
|
||||||
currency_zone TEXT DEFAULT 'TRACTUS_PRIMARY', -- TRACTUS_PRIMARY | MARK_PRIMARY | MIXED
|
currency_zone TEXT DEFAULT 'TRACTUS_PRIMARY', -- TRACTUS_PRIMARY | MARK_PRIMARY | MIXED
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Executable
+215
@@ -0,0 +1,215 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Extract a short narrative hook from every wiki/star-systems/<slug>/gttr.md
|
||||||
|
and store it on `star_systems.gttr_hook`.
|
||||||
|
|
||||||
|
The GTTR ("Drifter's Guide to the Reach") files follow a consistent
|
||||||
|
format: an H1 title, then a paragraph that opens with `**NAME**` followed
|
||||||
|
by a 1-3 sentence characterisation. That characterisation is the hook —
|
||||||
|
per D-191 §4 and the #833 naming pipeline it is the single strongest
|
||||||
|
cultural signal available for each system, capturing things like:
|
||||||
|
|
||||||
|
- Gateway: "the most connected system in the Reach, the site of its
|
||||||
|
oldest research institution, and the location of a gate that goes
|
||||||
|
to Earth and that nobody uses"
|
||||||
|
- Ran: "a very nice place to live, provided you are the sort of
|
||||||
|
person whose grandparents owned the land"
|
||||||
|
- ACB: "where the Lattice Commission lives, which means it is where
|
||||||
|
the rules live"
|
||||||
|
- Posto Avançado: "forward post — the place beyond the established
|
||||||
|
line"
|
||||||
|
|
||||||
|
This script parses each gttr.md, regex-extracts the first `**NAME**`
|
||||||
|
paragraph, normalises whitespace, and truncates at a soft word cap so
|
||||||
|
the hook stays cheap to inject into prompts. Empty or unmatched files
|
||||||
|
leave the column NULL.
|
||||||
|
|
||||||
|
Idempotent, safe to re-run after any wiki update. Explicit transaction
|
||||||
|
wrapper with rollback on exception.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
tooling/db/populate_gttr_hook.py
|
||||||
|
tooling/db/populate_gttr_hook.py --max-words 45
|
||||||
|
tooling/db/populate_gttr_hook.py --dry-run
|
||||||
|
tooling/db/populate_gttr_hook.py --system "GJ 71"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
REPO_ROOT = (SCRIPT_DIR / ".." / "..").resolve()
|
||||||
|
DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
|
||||||
|
WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems"
|
||||||
|
|
||||||
|
# Matches the first paragraph that opens with `**NAME**` at the start
|
||||||
|
# of a line. Captures everything up to a blank line or the next H1/H2.
|
||||||
|
# Non-greedy on the content.
|
||||||
|
# Match a line that opens with `**NAME**` (any non-asterisk chars, since
|
||||||
|
# names contain accents from many alphabets — Á, Ž, Ç, Ñ, etc) followed
|
||||||
|
# by the paragraph body up to a blank line or the next markdown heading.
|
||||||
|
_HOOK_RE = re.compile(
|
||||||
|
r"^\*\*([^\n*]+?)\*\*(.+?)(?:\n\n|\n#)",
|
||||||
|
re.DOTALL | re.MULTILINE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _system_slug_to_system_id(slug: str) -> str:
|
||||||
|
"""Convert 'GJ-244A' → 'GJ 244A'. Mirrors populate_terrain_reference.py."""
|
||||||
|
if slug.startswith("GJ-"):
|
||||||
|
return "GJ " + slug[3:]
|
||||||
|
return slug
|
||||||
|
|
||||||
|
|
||||||
|
def extract_hook(gttr_path: Path, max_words: int) -> str | None:
|
||||||
|
"""Return a compact one-paragraph hook for this gttr.md, or None if
|
||||||
|
the file is missing or the opening paragraph can't be located."""
|
||||||
|
if not gttr_path.exists():
|
||||||
|
return None
|
||||||
|
text = gttr_path.read_text()
|
||||||
|
|
||||||
|
match = _HOOK_RE.search(text)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
name_token = match.group(1).strip()
|
||||||
|
body = match.group(2).strip()
|
||||||
|
|
||||||
|
# Reconstruct "NAME is …" without the markdown asterisks. Add a
|
||||||
|
# single space between the name and the body since `.strip()` above
|
||||||
|
# removed the leading space from the captured body.
|
||||||
|
hook = f"{name_token} {body}"
|
||||||
|
|
||||||
|
# Collapse whitespace so multi-line paragraphs become one clean line.
|
||||||
|
hook = re.sub(r"\s+", " ", hook).strip()
|
||||||
|
|
||||||
|
# Strip a leading orphan "is" that comes from `**NAME**` + " is …":
|
||||||
|
# the regex captures the word "is" on its own because the opener is
|
||||||
|
# typically `**GATEWAY** (known as Tau Ceti...) is the most connected…`.
|
||||||
|
# Normal reading already works — this is just hygiene.
|
||||||
|
hook = re.sub(r"\s*\(\s*\)\s*", " ", hook)
|
||||||
|
|
||||||
|
# Hard cap at max_words. Truncate at the last word boundary before
|
||||||
|
# the cap and append an ellipsis so the reader knows it continues.
|
||||||
|
words = hook.split()
|
||||||
|
if len(words) > max_words:
|
||||||
|
hook = " ".join(words[:max_words]).rstrip(",;:") + "…"
|
||||||
|
|
||||||
|
return hook
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Populate star_systems.gttr_hook from wiki gttr.md files"
|
||||||
|
)
|
||||||
|
parser.add_argument("--db", default=str(DB_PATH), help="Path to systems.db")
|
||||||
|
parser.add_argument(
|
||||||
|
"--system",
|
||||||
|
help="Process only this system_id (e.g. 'GJ 71')",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-words",
|
||||||
|
type=int,
|
||||||
|
default=45,
|
||||||
|
help="Soft cap on hook length (default: 45 words)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Extract and print but do not write to the DB",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Print every extracted hook",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
db_path = Path(args.db)
|
||||||
|
if not db_path.exists():
|
||||||
|
print(f"error: {db_path} not found", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(str(db_path), timeout=30.0)
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
|
conn.execute("PRAGMA busy_timeout=15000")
|
||||||
|
conn.execute("PRAGMA foreign_keys=ON")
|
||||||
|
|
||||||
|
# Ensure the column exists — idempotent ADD COLUMN for old DBs.
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE star_systems ADD COLUMN gttr_hook TEXT")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass # column already exists
|
||||||
|
|
||||||
|
print(f"\n populate_gttr_hook.py")
|
||||||
|
print(f" DB: {db_path}")
|
||||||
|
print(f" max words: {args.max_words}")
|
||||||
|
if args.dry_run:
|
||||||
|
print(f" Mode: DRY RUN")
|
||||||
|
print()
|
||||||
|
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT system_id, proper_name FROM star_systems ORDER BY system_id"
|
||||||
|
).fetchall()
|
||||||
|
if args.system:
|
||||||
|
rows = [r for r in rows if r[0] == args.system]
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
missing = 0
|
||||||
|
unmatched = 0
|
||||||
|
|
||||||
|
conn.execute("BEGIN")
|
||||||
|
try:
|
||||||
|
for system_id, proper_name in rows:
|
||||||
|
slug = system_id.replace(" ", "-", 1)
|
||||||
|
gttr_path = WIKI_SYSTEMS / slug / "gttr.md"
|
||||||
|
|
||||||
|
hook = extract_hook(gttr_path, args.max_words)
|
||||||
|
if hook is None:
|
||||||
|
if not gttr_path.exists():
|
||||||
|
missing += 1
|
||||||
|
if args.verbose:
|
||||||
|
print(f" MISSING {system_id:10s} {gttr_path}")
|
||||||
|
else:
|
||||||
|
unmatched += 1
|
||||||
|
if args.verbose:
|
||||||
|
print(f" UNMATCHED {system_id:10s} {gttr_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
display = f"{system_id} ({proper_name})" if proper_name else system_id
|
||||||
|
print(f" {display}")
|
||||||
|
print(f" → {hook}")
|
||||||
|
|
||||||
|
if not args.dry_run:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE star_systems SET gttr_hook = ? WHERE system_id = ?",
|
||||||
|
(hook, system_id),
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
conn.rollback()
|
||||||
|
else:
|
||||||
|
conn.commit()
|
||||||
|
except BaseException:
|
||||||
|
conn.rollback()
|
||||||
|
conn.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print(f"\n Done:")
|
||||||
|
print(f" updated: {updated}")
|
||||||
|
print(f" missing: {missing}")
|
||||||
|
print(f" unmatched: {unmatched}")
|
||||||
|
if args.dry_run:
|
||||||
|
print(f"\n Dry run — no DB writes.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -137,44 +137,66 @@ class Logger:
|
|||||||
# source of corridor identity in systems.db (cultural_corridor is a
|
# source of corridor identity in systems.db (cultural_corridor is a
|
||||||
# legacy field that was never populated beyond sol-gateway-axis).
|
# legacy field that was never populated beyond sol-gateway-axis).
|
||||||
CORRIDOR_PALETTES: dict[str, dict[str, str]] = {
|
CORRIDOR_PALETTES: dict[str, dict[str, str]] = {
|
||||||
|
# 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": {
|
"core": {
|
||||||
# NOTE the style label is deliberately plain: earlier versions used
|
"inflection": "Gateway-era Earth diaspora, mostly English, "
|
||||||
# "institutional Latin / pan-Anglo / Gateway-era" which biased
|
"all Earth cultures welcome",
|
||||||
# Gemma 2 2B toward Latinate coinages like "Aureus" / "Aetheria".
|
"examples": "Meridian, Concord, East Ridge, Landing, Tanaka, "
|
||||||
# "Administrative English" gets prosaic output that matches the
|
"Rahman, Okafor, Ribeiro",
|
||||||
# settler-named frontier feel the core corridor actually has.
|
|
||||||
"inflection": "administrative English / Gateway-era",
|
|
||||||
"examples": "Meridian, Concord, East Ridge, Landing, Old Gate, Foreman's Run",
|
|
||||||
},
|
},
|
||||||
"north_reach": {
|
"north_reach": {
|
||||||
"inflection": "British / Australian / Irish",
|
"inflection": "British/Australian/Irish dominant, all Earth "
|
||||||
"examples": "Wolcott, Mildern, Ashbourne, Kiln, Briarfell, Tarndale",
|
"cultures welcome",
|
||||||
|
"examples": "Wolcott, Mildern, Ashbourne, Kiln, Briarfell, "
|
||||||
|
"Tarndale, Ribeiro, Nakamura, Kowalski",
|
||||||
},
|
},
|
||||||
"south_reach": {
|
"south_reach": {
|
||||||
"inflection": "Portuguese / Swahili / Cape Verdean / Brazilian",
|
"inflection": "Portuguese/Swahili/Cape Verdean/Brazilian "
|
||||||
"examples": "Vargas, Inhaca, Monteforte, Serra, Ribeiro, Kilimi",
|
"dominant, all Earth cultures welcome",
|
||||||
|
"examples": "Vargas, Inhaca, Monteforte, Serra, Ribeiro, "
|
||||||
|
"Kilimi, Holmberg, Fairview, Tanaka",
|
||||||
},
|
},
|
||||||
"east_reach": {
|
"east_reach": {
|
||||||
"inflection": "Korean / Japanese / Taiwanese",
|
"inflection": "Korean/Japanese/Taiwanese dominant, all Earth "
|
||||||
"examples": "Hanyang, Takamine, Seoraksan, Tsukuri, Ginoza, Baektu",
|
"cultures welcome",
|
||||||
|
"examples": "Hanyang, Takamine, Seoraksan, Tsukuri, Ginoza, "
|
||||||
|
"Baektu, Kellogg, Oliveira, Novak",
|
||||||
},
|
},
|
||||||
"west_reach": {
|
"west_reach": {
|
||||||
"inflection": "German / Dutch / Nordic",
|
"inflection": "German/Dutch/Nordic dominant, all Earth cultures "
|
||||||
"examples": "Vanebach, Kloosterdam, Bergfjord, Hellekade, Straend",
|
"welcome",
|
||||||
|
"examples": "Vanebach, Kloosterdam, Bergfjord, Hellekade, "
|
||||||
|
"Straend, Cooper, Vargas, Nakamura",
|
||||||
},
|
},
|
||||||
"deep_frontier": {
|
"deep_frontier": {
|
||||||
"inflection": "founder-surname + noun (e.g. 'Okafor Reach', 'Stenner Cross')",
|
"inflection": "frontier founder-name era, any Earth culture",
|
||||||
"examples": "Okafor Reach, Stenner Cross, Weller Hold, Pruitt Basin, Vickery Hold",
|
"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
|
# Legacy keys retained for backward compatibility with the
|
||||||
# cultural_corridor column on the one system that uses it.
|
# cultural_corridor column on the one system that uses it.
|
||||||
"sol-gateway-axis": {
|
"sol-gateway-axis": {
|
||||||
"inflection": "administrative English / Gateway-era",
|
"inflection": "Gateway-era Earth diaspora, mostly English, "
|
||||||
"examples": "Meridian, Concord, East Ridge, Landing, Old Gate, Foreman's Run",
|
"all Earth cultures welcome",
|
||||||
|
"examples": "Meridian, Concord, Cardinal, Prefecture, Tanaka, "
|
||||||
|
"Rahman, Okafor",
|
||||||
},
|
},
|
||||||
"inner_corridor": {
|
"inner_corridor": {
|
||||||
"inflection": "administrative English",
|
"inflection": "Gateway-era Earth diaspora, mostly English, "
|
||||||
"examples": "Meridian, Concord, East Ridge, Landing, Old Gate",
|
"all Earth cultures welcome",
|
||||||
|
"examples": "Meridian, Concord, East Ridge, Landing, Tanaka, "
|
||||||
|
"Rahman, Okafor",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,6 +679,9 @@ def _build_prompt(
|
|||||||
body_id: str,
|
body_id: str,
|
||||||
local_id: str,
|
local_id: str,
|
||||||
attempt: int,
|
attempt: int,
|
||||||
|
system_hook: str | None = None,
|
||||||
|
system_name: str | None = None,
|
||||||
|
body_name: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Assemble a few-shot prompt for the given feature type.
|
"""Assemble a few-shot prompt for the given feature type.
|
||||||
|
|
||||||
@@ -664,6 +689,15 @@ def _build_prompt(
|
|||||||
(body_id, local_id, attempt) — this puts a finger on the sampling
|
(body_id, local_id, attempt) — this puts a finger on the sampling
|
||||||
scales so neighbouring features on the same body don't all draw
|
scales so neighbouring features on the same body don't all draw
|
||||||
from an identical prompt and collapse to identical outputs.
|
from an identical prompt and collapse to identical outputs.
|
||||||
|
|
||||||
|
`system_hook` is the pre-extracted gttr.md characterisation for
|
||||||
|
this system (e.g. "RAN is, by all accounts, a very nice place to
|
||||||
|
live, provided you are the sort of person whose grandparents owned
|
||||||
|
the land"). When present it gets woven into the preamble so Gemma
|
||||||
|
has per-system cultural context — this is the single biggest lever
|
||||||
|
for producing names that feel like they belong to THIS world rather
|
||||||
|
than any world in this corridor. When absent the prompt degrades to
|
||||||
|
the corridor inflection alone.
|
||||||
"""
|
"""
|
||||||
cfg = _PROMPT_CONFIG.get(feature_type)
|
cfg = _PROMPT_CONFIG.get(feature_type)
|
||||||
if cfg is None:
|
if cfg is None:
|
||||||
@@ -684,6 +718,25 @@ def _build_prompt(
|
|||||||
# singular possessive ones ("their capital town"). Heuristic: if the
|
# singular possessive ones ("their capital town"). Heuristic: if the
|
||||||
# subject starts with "their", use "called"; otherwise "named".
|
# subject starts with "their", use "called"; otherwise "named".
|
||||||
verb = "called" if subject.startswith("their ") else "named"
|
verb = "called" if subject.startswith("their ") else "named"
|
||||||
|
|
||||||
|
# Context block. When system / body proper names are available,
|
||||||
|
# label them explicitly so Gemma has the canonical identifiers in
|
||||||
|
# addition to the gttr hook. Redundancy helps on a small model —
|
||||||
|
# the hook already opens with the system name but putting it in a
|
||||||
|
# labeled "System:" line makes it harder to gloss over. When the
|
||||||
|
# gttr hook exists too, it goes on its own line as the cultural
|
||||||
|
# one-liner.
|
||||||
|
context_lines: list[str] = []
|
||||||
|
ident_parts = []
|
||||||
|
if system_name:
|
||||||
|
ident_parts.append(f"System: {system_name}")
|
||||||
|
if body_name:
|
||||||
|
ident_parts.append(f"Planet: {body_name}")
|
||||||
|
if ident_parts:
|
||||||
|
context_lines.append(". ".join(ident_parts) + ".")
|
||||||
|
if system_hook:
|
||||||
|
context_lines.append(f"About the system: {system_hook}")
|
||||||
|
|
||||||
preamble = (
|
preamble = (
|
||||||
f"Settlers {verb} {subject} after themselves, after what they saw, "
|
f"Settlers {verb} {subject} after themselves, after what they saw, "
|
||||||
f"or after places back home. Most names are mundane, short, and "
|
f"or after places back home. Most names are mundane, short, and "
|
||||||
@@ -694,6 +747,9 @@ def _build_prompt(
|
|||||||
)
|
)
|
||||||
|
|
||||||
lines = [preamble, ""]
|
lines = [preamble, ""]
|
||||||
|
if context_lines:
|
||||||
|
lines.extend(context_lines)
|
||||||
|
lines.append("")
|
||||||
for style, example in pool:
|
for style, example in pool:
|
||||||
lines.append(f"Style: {style}. Answer: {example}")
|
lines.append(f"Style: {style}. Answer: {example}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -1207,6 +1263,7 @@ def name_feature(
|
|||||||
body_used: set[str],
|
body_used: set[str],
|
||||||
stem_counts: dict[str, int],
|
stem_counts: dict[str, int],
|
||||||
stem_cap: int,
|
stem_cap: int,
|
||||||
|
system_hook: str | None,
|
||||||
world_seed: int,
|
world_seed: int,
|
||||||
body_id: str,
|
body_id: str,
|
||||||
local_id: str,
|
local_id: str,
|
||||||
@@ -1272,6 +1329,9 @@ def name_feature(
|
|||||||
body_id=body_id,
|
body_id=body_id,
|
||||||
local_id=local_id,
|
local_id=local_id,
|
||||||
attempt=attempt,
|
attempt=attempt,
|
||||||
|
system_hook=system_hook,
|
||||||
|
system_name=ctx.get("system_proper_name"),
|
||||||
|
body_name=ctx.get("body_proper_name"),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
raw = voice.request(prompt, seed)
|
raw = voice.request(prompt, seed)
|
||||||
@@ -1359,6 +1419,26 @@ def load_body_context(body_id: str, system_id: str, conn: sqlite3.Connection) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_system_gttr_hooks(conn: sqlite3.Connection) -> dict[str, str]:
|
||||||
|
"""Return `{system_id: gttr_hook}` for every system with a populated
|
||||||
|
hook. The gttr_hook column is filled by
|
||||||
|
`tooling/db/populate_gttr_hook.py` from wiki/star-systems/<slug>/gttr.md
|
||||||
|
and captures the system's canonical characterisation in 30-45 words
|
||||||
|
("where the rules live", "forty years old and still in the draft",
|
||||||
|
…). The naming pipeline injects this into the prompt preamble so
|
||||||
|
Gemma has per-system cultural context without having to parse
|
||||||
|
thousands of lines of wiki markdown at runtime.
|
||||||
|
|
||||||
|
Systems whose gttr_hook is NULL return nothing from this map — the
|
||||||
|
caller falls back to the corridor palette alone.
|
||||||
|
"""
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT system_id, gttr_hook FROM star_systems "
|
||||||
|
"WHERE gttr_hook IS NOT NULL AND gttr_hook != ''"
|
||||||
|
).fetchall()
|
||||||
|
return {row[0]: row[1] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
def load_body_hop_order(conn: sqlite3.Connection) -> dict[str, tuple[int, str]]:
|
def load_body_hop_order(conn: sqlite3.Connection) -> dict[str, tuple[int, str]]:
|
||||||
"""Return a `{body_id: (hop_distance_from_gateway, body_id)}` map used
|
"""Return a `{body_id: (hop_distance_from_gateway, body_id)}` map used
|
||||||
as a stable sort key so the pipeline walks the reach from core
|
as a stable sort key so the pipeline walks the reach from core
|
||||||
@@ -1416,6 +1496,7 @@ def process_body(
|
|||||||
corpus: dict[tuple[str, str], set[str]],
|
corpus: dict[tuple[str, str], set[str]],
|
||||||
stem_counts: dict[str, int],
|
stem_counts: dict[str, int],
|
||||||
stem_cap: int,
|
stem_cap: int,
|
||||||
|
system_hook: str | None,
|
||||||
world_seed: int,
|
world_seed: int,
|
||||||
log: "Logger",
|
log: "Logger",
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
@@ -1474,7 +1555,7 @@ def process_body(
|
|||||||
feature_type = _feature_type_for_city(city)
|
feature_type = _feature_type_for_city(city)
|
||||||
name = name_feature(
|
name = name_feature(
|
||||||
voice, feature_type, ctx, blocklist, corpus, body_used,
|
voice, feature_type, ctx, blocklist, corpus, body_used,
|
||||||
stem_counts, stem_cap,
|
stem_counts, stem_cap, system_hook,
|
||||||
world_seed, body_id, city.get("id") or "city_?",
|
world_seed, body_id, city.get("id") or "city_?",
|
||||||
log, verbose,
|
log, verbose,
|
||||||
)
|
)
|
||||||
@@ -1491,7 +1572,7 @@ def process_body(
|
|||||||
continue
|
continue
|
||||||
name = name_feature(
|
name = name_feature(
|
||||||
voice, "river", ctx, blocklist, corpus, body_used,
|
voice, "river", ctx, blocklist, corpus, body_used,
|
||||||
stem_counts, stem_cap,
|
stem_counts, stem_cap, system_hook,
|
||||||
world_seed, body_id, river.get("id") or "river_?",
|
world_seed, body_id, river.get("id") or "river_?",
|
||||||
log, verbose,
|
log, verbose,
|
||||||
)
|
)
|
||||||
@@ -1511,7 +1592,7 @@ def process_body(
|
|||||||
feature_type = _feature_type_for_ocean(water)
|
feature_type = _feature_type_for_ocean(water)
|
||||||
name = name_feature(
|
name = name_feature(
|
||||||
voice, feature_type, ctx, blocklist, corpus, body_used,
|
voice, feature_type, ctx, blocklist, corpus, body_used,
|
||||||
stem_counts, stem_cap,
|
stem_counts, stem_cap, system_hook,
|
||||||
world_seed, body_id, water.get("id") or "water_?",
|
world_seed, body_id, water.get("id") or "water_?",
|
||||||
log, verbose,
|
log, verbose,
|
||||||
)
|
)
|
||||||
@@ -1530,7 +1611,7 @@ def process_body(
|
|||||||
continue
|
continue
|
||||||
name = name_feature(
|
name = name_feature(
|
||||||
voice, "mountain_range", ctx, blocklist, corpus, body_used,
|
voice, "mountain_range", ctx, blocklist, corpus, body_used,
|
||||||
stem_counts, stem_cap,
|
stem_counts, stem_cap, system_hook,
|
||||||
world_seed, body_id, rng_feat.get("id") or "range_?",
|
world_seed, body_id, rng_feat.get("id") or "range_?",
|
||||||
log, verbose,
|
log, verbose,
|
||||||
)
|
)
|
||||||
@@ -1550,7 +1631,7 @@ def process_body(
|
|||||||
feature_type = _feature_type_for_poi(poi)
|
feature_type = _feature_type_for_poi(poi)
|
||||||
name = name_feature(
|
name = name_feature(
|
||||||
voice, feature_type, ctx, blocklist, corpus, body_used,
|
voice, feature_type, ctx, blocklist, corpus, body_used,
|
||||||
stem_counts, stem_cap,
|
stem_counts, stem_cap, system_hook,
|
||||||
world_seed, body_id, poi.get("id") or "poi_?",
|
world_seed, body_id, poi.get("id") or "poi_?",
|
||||||
log, verbose,
|
log, verbose,
|
||||||
)
|
)
|
||||||
@@ -1725,6 +1806,7 @@ def main():
|
|||||||
ensure_atlas_schema(conn)
|
ensure_atlas_schema(conn)
|
||||||
|
|
||||||
hop_order = load_body_hop_order(conn)
|
hop_order = load_body_hop_order(conn)
|
||||||
|
system_gttr_hooks = load_system_gttr_hooks(conn)
|
||||||
markers_paths = discover_bodies(args.body, args.limit, hop_order)
|
markers_paths = discover_bodies(args.body, args.limit, hop_order)
|
||||||
if not markers_paths:
|
if not markers_paths:
|
||||||
log(f"error: no markers.json found (body={args.body})")
|
log(f"error: no markers.json found (body={args.body})")
|
||||||
@@ -1824,6 +1906,7 @@ def main():
|
|||||||
corpus=corpus,
|
corpus=corpus,
|
||||||
stem_counts=stem_counts,
|
stem_counts=stem_counts,
|
||||||
stem_cap=args.stem_cap,
|
stem_cap=args.stem_cap,
|
||||||
|
system_hook=system_gttr_hooks.get(system_id),
|
||||||
world_seed=args.seed,
|
world_seed=args.seed,
|
||||||
log=log,
|
log=log,
|
||||||
verbose=args.verbose,
|
verbose=args.verbose,
|
||||||
|
|||||||
Reference in New Issue
Block a user