`reach wiki stats` and `reach wiki gttr-hook` replace tooling/db/wiki_sync.py and populate_gttr_hook.py. Both are output-identical to the originals: `stats` byte-for-byte, and all 301 extracted GTTR hooks line-for-line. wiki_sync.py moved whole, but generate_wiki() and import_from_wiki() are NOT verbs. Before porting, the old `--generate` was run against a clean tree to get a parity baseline. It changed all 301 system pages, +940 / -10,761, and was reverted at once. It deletes the Celestial Bodies / Stations blocks (owned by the Rust atlas sync, which it does not know about), deletes the Industries / Exports / Imports rows (nothing writes those any more), and rewrites star types where systems.db and the pages disagree. D-262, CLAUDE.md and the wiki skill all described it as the routine, prose-preserving render. CLAUDE.md and the skill now say not to run it; D-262 needs amending — T-1292. Provenance moves to tooling/archive/, with a README naming what each script did and why it is not run: - pql-migrate/ (the T-1271 ruling) - wiki-bootstrap/: assign-astro-ids + its catalog, migrate-s-to-gj, patch-core-sector (hardcodes a dead path), fill-missing-globes, generate-stubs and find-stubs (finds 0 stubs — Phase 1 is done), backfill_cultural_corridor (a raw systems.db patch script, outside D-262), and process-wiki-system-changes, whose last step is the destructive render Also: - stats() printed "run import first" and exited 0 when a table was missing; it now fails with a remedy. generate_wiki() counted created pages after writing them, so `created` was always 0. - tooling/godot-cold-parse and godot-parse-sweep were never retired after T-1283, and the pr-process skill still told agents to run them. Removed; the skill and parse_sweep.gd now name the reach verbs. - systems.db re-stamped: schema comments changed, and the stamp records the schema file's SHA for tamper detection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
174 lines
6.1 KiB
Python
Executable File
174 lines
6.1 KiB
Python
Executable File
"""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 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. It writes systems.db directly, which the
|
|
asset-pipeline rule otherwise forbids; this edge is sanctioned by the D-262
|
|
flow diagram (`populate_gttr_hook -> systems.db`), and `gttr_hook` sits on
|
|
`star_systems`, which `reach ledger import` does not clear.
|
|
|
|
Formerly tooling/db/populate_gttr_hook.py (T-1290).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
from tooling.core import config, console
|
|
from tooling.core.errors import ReachError
|
|
|
|
REPO_ROOT = config.repo_root()
|
|
DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
|
|
WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems"
|
|
|
|
# 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()
|
|
|
|
# Drop an empty parenthetical left behind by the collapse — hygiene only.
|
|
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 run(
|
|
db_path: Path = DB_PATH,
|
|
*,
|
|
system: str | None = None,
|
|
max_words: int = 45,
|
|
dry_run: bool = False,
|
|
verbose: bool = False,
|
|
) -> dict[str, int]:
|
|
"""Populate star_systems.gttr_hook. Returns updated/missing/unmatched counts."""
|
|
if not db_path.exists():
|
|
raise ReachError(
|
|
f"{db_path} not found",
|
|
fix="pass --db with an existing systems.db, or restore it: git restore server/data/systems.db",
|
|
)
|
|
|
|
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
|
|
|
|
console.event(
|
|
f"gttr-hook into {db_path} (max {max_words} words)" + (" — DRY RUN" if dry_run else "")
|
|
)
|
|
|
|
rows = conn.execute("SELECT system_id, proper_name FROM star_systems ORDER BY system_id").fetchall()
|
|
if system:
|
|
rows = [r for r in rows if r[0] == system]
|
|
if not rows:
|
|
conn.close()
|
|
raise ReachError(
|
|
f"no system {system!r} in star_systems",
|
|
fix="pass a system_id with its space, e.g. --system 'GJ 71'",
|
|
)
|
|
|
|
counts = {"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, max_words)
|
|
if hook is None:
|
|
kind = "missing" if not gttr_path.exists() else "unmatched"
|
|
counts[kind] += 1
|
|
if verbose:
|
|
console.event(f"{kind.upper()} {system_id} {gttr_path}", level="warn")
|
|
continue
|
|
|
|
if verbose:
|
|
display = f"{system_id} ({proper_name})" if proper_name else system_id
|
|
console.out(f"{display}\n → {hook}")
|
|
|
|
if not dry_run:
|
|
conn.execute(
|
|
"UPDATE star_systems SET gttr_hook = ? WHERE system_id = ?",
|
|
(hook, system_id),
|
|
)
|
|
counts["updated"] += 1
|
|
|
|
if dry_run:
|
|
conn.rollback()
|
|
else:
|
|
conn.commit()
|
|
except BaseException:
|
|
conn.rollback()
|
|
conn.close()
|
|
raise
|
|
|
|
conn.close()
|
|
return counts
|