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.
216 lines
7.0 KiB
Python
Executable File
216 lines
7.0 KiB
Python
Executable File
#!/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()
|