CultureResolver with Arc<Mutex<Connection>> over systems.db (SQLITE_OPEN_READ_ONLY).
3-pass lookup: system_id → body_id (COALESCE parent fallback) → station_id.
CultureResolverResource registered in main.rs with graceful warn-on-missing.
BookmarkRegistry.build_catalog() uses resolver for allowed_locations_cultures.
8 unit tests including concurrent safety. SQLite fixture at
server/src/knowledge/fixtures/culture_test.db.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sprint goal: close Phase 3 Atlas (unified nav chain, brand corps,
content refinement) and establish Phase 4 foundations (bookmark system,
location-culture resolution, character creation skeleton).
15 tickets assigned across server (7), client (5), copy (3).
Briefings written for all four teams. DB backup updated.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Fix "Gemma 2 GGUF" in user-facing error message (line 2073)
- Fix gemma2.gguf in docstring usage example (line 32)
- Fix O(N) _is_duplicate: pre-build lowercase shadow sets for O(1) lookup
- Expand vestigial note to enumerate full ~750-line dead island boundaries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hoshe:
- Mark --dump-prompts / name_feature() as vestigial with TODO note
- Fix --refresh help string: 200 → 1000 (matches actual default)
- Fix _RIVER_POOLS comment numbering: Pool 6 before Pool 5 → correct order
- Remove dead first-pass code in fix_fewshot_bleed.py
- _CAPTURE_FILE leak noted in vestigial TODO
Tyre:
- Fix stale "Gemma 2" strings in banner, argparse description, model help
- Note dead code for cleanup pass (name_feature ~700 lines)
Hoshe (prune):
- prune_atlas_features.py: named features sort before unnamed, preventing
silent discard of hand-authored names during pruning
naming_core:
- v0.2: few-shot blocklist, stricter is_valid_name (min 3 chars, no digits,
no brackets), prompt fragment rejection expanded
Miri clarification: the 261 "empty-string" files contain only roads (37)
and railroads (37) — infrastructure features never in naming scope. All
cities/rivers/oceans/mountains/POIs are clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full architecture doc covering the Gemma 4 batch naming pipeline:
pipeline stages, cultural registers, body ordering, known limitations,
QA process, and extension guide.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the one-at-a-time Gemma 2 naming pipeline with a batch-oriented
Gemma 4 E2B pipeline. Key changes:
- naming_core.py: shared library with Levenshtein distinctiveness ranking,
batch prompt building, mood injection pool, name validation, and
adjacent-register refill logic
- Wiki-grounded register selection: per-system LLM call picks the cultural
register based on wiki/GTTR content instead of hash randomizer
- Batch naming: requests N*2 names per call, ranks by word-average
Levenshtein distance, fills quota from most-distinct candidates
- Mood pool: 13 emotional seeds randomized per-body for vocabulary
divergence (ambition, fear, isolation, defiance, etc.)
- Adjacent-register refill: when primary register exhausts, automatically
switches to next corridor substyle
- Inhabited-first body ordering: habitable worlds get first pick of
register vocabulary, barren moons get leftovers
- Process group cleanup: SIGTERM/SIGKILL the full distrobox chain on
subprocess refresh to prevent GPU zombie processes
- qa_naming.py: QA report, fix_fewshot_bleed.py: post-hoc fix script
- test_batch_naming.py, test_register_selection.py: test harnesses
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Individual dedup/blocklist/placeholder/empty rejections that recover
on the next attempt are now silent. Only the skipped: summary line
prints when all 5 attempts fail. Subprocess errors still print
immediately (those indicate a real problem).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bumped max_attempts from 3 to 5 — with per-system dedup and no stem
cap, the remaining dedup hits are mostly per-body collisions which
a couple extra attempts with rotated pools can escape.
Bumped --refresh default from 200 to 1000. Fewer subprocess restarts
= fewer model reloads via distrobox. KV-cache bleed risk is lower
now that the validation gauntlet is lighter.
Reverted the batch-prompt experiment — Gemma 2 2B drifts on
multi-line output; individual calls are more reliable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The stem cap (--stem-cap 20) was rejecting valid names because common
feature-type vocabulary tokens like "ridge", "hill", "range" hit the
cap after ~200 bodies and blocked all subsequent names containing
them. With sub-style rotation already providing variety, the cap was
doing more harm than good. Removed entirely.
Cross-body dedup narrowed from (hop, corridor, feature_type) to
(system_id, feature_type). Two rivers in the same system can't share
a name; two rivers in different systems can. This matches how
settlers actually name things — they don't coordinate with other
star systems.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
When all 3 LLM attempts are rejected (dedup, blocklist, etc.),
name_feature now returns None instead of a deterministic palette
fallback. process_body leaves the name as null in markers.json.
The preserved path (_is_blank) treats null as unnamed, so a fill
round (re-running the script) picks up only the skipped features
with a fresh corpus — zero dedup pressure from the first pass. The
fill round can use a different seed, slower prompt, or a different
backend entirely (e.g. Haiku).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
The upstream terrain pipeline assigns sparse IDs (range_1, range_50,
range_29...) and the prune pass drops entries but keeps original IDs.
This leaves 2394 bodies with non-sequential IDs across mountain_ranges,
rivers, and oceans.
Renumbered all feature IDs to sequential {prefix}_0, {prefix}_1, ...
preserving sort order. 24021 IDs fixed across 2394 bodies. No name
or geometry data changed — only the id field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Retry rejection lines (dedup, blocklist, placeholder, stem_cap, empty,
error) now print unconditionally, not only under --verbose. The
fallback line also includes a tally of the rejection reasons that
exhausted all attempts, e.g.:
fallback: GJ144e-1/range_43 → 'Kirkwood Spine' [blocklist=2 dedup=1]
Diagnostic run on 20 bodies confirms dedup is the primary fallback
driver. Gemma converges on a narrow set of range names ("The Ridge",
"Blackwood Range", "The Spine") that collide across bodies in the
same corridor. Blocklist catches "Thames" and "The Great Divide"
correctly. Zero stem-cap or subprocess-error fallbacks observed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The gfx1201 preflight check piped `strings` into grep, which fails
silently on a Bazzite host where binutils is not installed and
`strings` is not on PATH. `grep -a` reads the binary directly as
text, works everywhere grep exists, and produces the same result.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wraps gemma_naming.py with the validated overnight recipe: gfx1201
ROCm binary path, distrobox reach-build for libhipblas at runtime,
timestamped log under .tmp/.
Preflight checks: binary exists and is executable, model present,
reach-build container exists, binary strings contains gfx1201 kernels.
Fails fast on any missing prerequisite so a broken build can't waste
an overnight window. Script takes no arguments; anything passed is
rejected so a stray --help can't accidentally launch the pipeline.
Estimate ~4-6 h for ~26k features across 2394 bodies at 74 t/s on an
RX 9070. Safe to interrupt and resume — preserved path skips
already-named bodies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three additions unlocked by the gfx1201 ROCm debug session.
1. _find_sr_voice() resolves the default binary path to
~/Projects/settled-reach/binaries/sr-voice-rocm (persistent across
worktree lifetimes) with a legacy fallback to the main workdir's
cargo target dir. Matches #850's plan to ship platform binaries
outside the repo.
2. --distrobox <name> wraps the sr-voice subprocess in
`distrobox enter <name> --` when the built binary depends on libs
that only exist inside a dev container (libhipblas.so.2 on a
Bazzite host). Stdio JSONL protocol flows through unchanged.
3. --dump-prompts PATH captures the attempt-0 prompt for every
feature as JSONL without calling an LLM. Force --mock and
short-circuit name_feature to return a unique deterministic
placeholder. Used to feed the same prompt set to alternate
backends (Haiku agent, other models) for offline A/B comparison
of naming quality independent of the sampling backend.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LlamaModelParams::default() sets n_gpu_layers=0, so even with --features
rocm the model ran entirely on CPU at ~19 t/s. Setting n_gpu_layers to a
large sentinel value asks llama.cpp to offload every layer the model
has; llama.cpp clamps to the real count (27 for Gemma 2 2B). Observed
throughput jumps from 19 t/s to 74 t/s on an RX 9070 once the ROCm
binary is also compiled for gfx1201 (see tooling commit).
Also adds server/sr-voice/.gitignore so locally-built binaries don't
sneak into the worktree. Release binaries ship out-of-tree per #850.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous runs (both the GPU-contention kill and the anglophone-only
interrupt) left 19 hop 0-1 core bodies with stale generator output in
their markers.json files. Those bodies were being skipped via the
preserved path on relaunch, which meant the gttr-context fix
(commit a5fbce4c) would never touch them — exactly the set of
high-visibility systems that benefits most from per-system cohesion.
Reset to origin/main (clean null-name state) + re-prune to the 8/6
caps. Hand-authored templates (Edict, Estrade, Vuurkloof, Lendel,
Cairnside, Røros) explicitly excluded from the reset list and
verified intact (2-4 named cities each, untouched).
After this commit only the 6 hand-authored templates have
populated names in wiki/star-systems/. The entire rest of the reach
is clean and will be freshly named by the next gemma_naming.py run
with the full gttr + cosmopolitan + grounded few-shot + rotating
pool stack.
Bodies reset:
- Ran (GJ 144): all 9 bodies
- Sirius (GJ 244A): GJ244Ab, c, e-1, e-2 (not Ad, that's Edict)
- ACB (GJ 559B): GJ559Bb
- Tau Ceti (GJ 71): GJ71b, c, d, d-1, e
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.
Two fixes from observing the first Gemma 2 batch run on Sirius:
1) Prune oversized feature counts. The upstream terrain pipeline emits
every distinct mountain cluster as a separate `mountain_range` and
every flowing path as a separate `river`. At the atlas generator's
512×256 grid this produced bodies with 40-80 named ranges and
10-15 rivers — noise, not information. A single planet with 48
ridges isn't richer, it's unparseable.
`tooling/planet-gen/prune_atlas_features.py` walks every
`markers.json` under `wiki/star-systems/`, ranks each feature type
by a size proxy, and keeps only the top N:
- mountain_ranges: sorted by `area_cells`, top 8 per body
- rivers: sorted by path length, top 6 per body
- oceans / cities / pois: untouched (already small, or
hand-authored by generate_atlas.py)
Sol (GJ-0) is hardcoded-excluded from pruning so the hand-authored
Earth / Mars / moon content stays untouched.
Each pruned body gets its atlas_* rows re-synced via
`sync_markers_to_db` so the DB mirror stays consistent. Bodies
whose wiki folder has no matching row in `bodies` (14 pre-existing
orphans like GJ1156h-1, GJ34Ah-2, …) are pruned in-file but skip
the DB sync to avoid FK violations on atlas_body_grids.
First run results:
bodies scanned: 2394
bodies pruned: 1513
mountain ranges dropped: 11640
rivers dropped: 1382
Safe to re-run — idempotent when a body is already within the caps.
2) Grounded cosmopolitan fallback palette. When Gemma's 3 retries
all fail (dedup, blocklist, stem-cap, placeholder), the code falls
to `_FALLBACK_STEMS[corridor]`. The old table had 10 stems per
corridor, all Latin-institutional (Meridian, Concord, Prefecture,
Cardinal, Lumen, Foro, Tabula, Vox, Axis, Senatus), which produced
the same-y `Axis Spine / Axis Ridge / Axis Heights / Axis Scarp`
clusters the user flagged on Sirius — exactly the old epic-Latin
register the few-shot pools were rewritten to avoid.
Fallbacks now draw from a 30-45 stem grounded cosmopolitan list
per corridor matching the few-shot pool intent:
- core: 45 stems (Ashfield, Bellview, Cedarbrook,
Fairmont, Ironwood, Kirkwood, Linden, Meridian,
Northfield, Riverside, Westbrook, …)
- north_reach: 40 stems (Ashford, Bellfield, Clifford, Drayton,
Elmhurst, Garner, Holmwood, Kelsworth, …)
- west_reach: 35 stems (Altdorf, Bergfjord, Eikhof, Hoogland,
Järvenpää, Kloosterdam, Nieuwpoort, Sørholm,
Svarteberg, Torsfell, Voorhout, Weserhof, Östby, …)
- east_reach: 35 stems (Aomori, Baektu, Chōshi, Fukagawa,
Hanyang, Izumi, Takamine, Yurigawa, …)
- south_reach: 36 stems (Alves, Brandão, Évora, Gomes, Ribeiro,
Serra, Várzea, Hlanganani, Kilimi, …)
- deep_frontier: 30 stems (Okafor, Stenner, Weller, Kellogg,
Stonebrook, Dustgate, Blackwater, …)
Per-feature suffix lists also expanded (e.g. river suffixes now
include Brook, Stream, Flow, Creek on top of the original Run /
Water / Beck / Rill / Course). Net effect: 300-450 unique fallback
combinations per (corridor, feature_type), up from 50, in the same
grounded register the few-shot pools teach.
Also preserves aliases `inner_corridor`, `inner_orbit`, and
`sol-gateway-axis` as legacy-compatible keys pointing at the
administrative-English palette.
Combined effect on the next run:
- ~45% fewer features to name (pruned 13k/52k)
- ~9× more fallback variety per corridor when fallback does trigger
- Same grounding overhaul from the previous commit, now reaching
into the safety-net path
Substantial quality pass on gemma_naming.py driven by user review of
the first real-mode smoke test output. The earlier run produced names
that read too sci-fi / epic-fantasy / same-y: Aureus, Aetheria,
Stellaris, Nexus, Elysium. Root cause analysis + fixes:
1. Runtime timestamps. The log prefix is now
`[HH:MM:SS +00h03m]` — clock time plus elapsed-since-start. Gives
the user an at-a-glance sense of how long the run has been going
without scrolling back to the banner.
2. System / body headers. When the loop enters a new system it prints
`── SYSTEM K/N GJ 71 — Tau Ceti (hop 0)`. Each body line now
shows `GJ71c (Threshold)` if the body has a proper_name in
systems.db, so the log reads like a tour of the reach rather than
a wall of body_id slugs. Preserved (already-named) bodies now log
a compact "(skip — N names already set)" line so progress is
visible even when no inference happened.
3. Prompt grounding overhaul. The old few-shot examples were all
classical/epic (Wolcott Beck, Nakamura Stream, Ribeiro do Sal,
Drayton Spine) which biased Gemma 2 2B toward Latin/Greek
coinages. New preambles use the shape:
"Settlers named X after themselves, after what they saw, or
after places back home. Most names are mundane, short, and
direct — a surname, a compass direction, a feature, a
practical description. Classical or epic names are rare."
Combined with grounded example pools, Gemma now produces names
like "Cooper's Creek", "Western Ridge", "The Highroad",
"Blackwood Creek", "Dustbowl".
4. Core corridor relabel. The "core" palette inflection was
"institutional Latin / pan-Anglo / Gateway-era", which pattern-
matched in Gemma's training data to "make up Latin-sounding
words" (→ Ardenia, Aurelia, Stellaris). Now it's
"administrative English / Gateway-era" and the outputs are
prosaic — Port Dundas, East Ridge, Meridian, Landing.
5. Rotating few-shot example pools. Each feature type now has 5-7
pools of 5-6 examples each. `_build_prompt()` picks a pool
deterministically per (body_id, local_id, attempt) so:
- Same feature always gets the same prompt (determinism preserved).
- Neighbouring features on the same body get different prompts
(output variance — the sampler doesn't collapse to a single
mode when you ask for 16 mountain names in a row).
- Retries rotate to a new pool, not just a bumped seed, giving
dedup failures a clean second attempt.
6. Cosmopolitan cultural variety in the examples. Earlier pools only
showed British/Australian, Korean/Japanese, Portuguese/Swahili,
German/Dutch/Nordic axes — the four reach corridors. Gemma learned
"names come in four flavours". New pools span Dutch, Nordic,
Italian, French, Polish, Hungarian, Czech, Spanish, Russian,
Finnish, Greek, Irish, Japanese, and British — teaching the model
that names can be any real Earth cultural register, not just the
corridor label. The result: actual Dutch names (Egelantier,
Hochland, Van Damhoeve), actual Nordic (Lundstad, Brygga),
actual Italian (Borgo Marconi, Piazza Nuova), etc.
7. First-name possessive pools. Per user feedback, settler naming
includes both surnames ("Cooper's Creek") and first names
("Clifford's Bay", "Maura's Run", "Yuki's Pool"). Each feature
type now has a dedicated first-name-possessive pool in addition
to the existing surname pool — the two rotate alongside so both
patterns show up without either dominating.
8. One "classical/Latinate" pool per feature type (≈17% of calls
given 5-7 pools per type). Keeps occasional Latin flavour without
making it dominant — the user explicitly noted that replacing
one pattern with another "is never a clean fix for a randomizer."
9. Earth-name blocklist expansion. The Gemma 2 model reached for
real European names ("Weser", "Rhine", "Reykjavik") in the first
real run. Added 21 European rivers (Rhine, Weser, Elbe, Oder,
Vistula, Loire, Rhône, Douro, Tagus, Ebro, Po, Arno, Tiber, …)
and 25 Nordic/Eastern European cities (Reykjavik, Oslo, Gdansk,
Krakow, Prague, Warsaw, Budapest, Belgrade, …). Case-insensitive
"The <name>" stripping still applies so "The Great Divide" also
matches "Great Divide".
Combined smoke test after these changes (10 real-mode prompts across
core + west_reach):
- core: Port Dundas, The Backbone, Dustbowl, Blackwood Creek
- west_reach: Egelantier, Hochland, Der Rücken, Lundstad, Klipfjord
- no placeholder residue, no markdown, no 5+ word outputs.
--shard is gone (dead code since GPU contention killed parallelism).
Resume semantics are still free: re-run the same command and
already-named bodies skip via the preserved path.
Parallelism via two concurrent sr-voice subprocesses does not work on
this ROCm + llama-cpp-rs setup — launching a second instance poisons
the first one's GPU context (both fall back to 0% GPU / 50% CPU
busy-loop and stop making progress). Verified empirically: single
shard runs cleanly at ~1.2s/feature, two shards deadlock.
Without a working parallel path, --shard is dead weight. Resume
semantics were already free: the pipeline skips bodies whose
markers.json has non-empty name fields (preserved path), so a
killed run re-starts just by re-running the same command.
Simplifications:
- Remove --shard argument and all slicing logic.
- Remove banner_shard / shard_offset / shard_n / shard_m plumbing.
- Rename internal total_shard_systems → total_systems.
- Default --log path is now .tmp/gemma_naming.log (was conditional
on --shard). Pass `--log -` to disable file logging.
- Startup banner now prints a one-line resume reminder so the user
can see at a glance that a killed run is recoverable.
New end-to-end pipeline that walks every markers.json in the reach and
fills empty `name` fields using the Gemma 2 voice pipeline via
`sr-voice serve --stdio`. Per D-191 §4: the same Gemma 2 pipeline the
client uses for NPC voicing also produces the atlas content, which is
dual-purposed as a quality test of the LLM plumbing.
Pipeline per body (hop-ordered, core-first):
1. Load markers.json; identify feature records whose `name` is
blank (null or ""). Hand-authored names are never overwritten;
the 6 template bodies and any partial authoring stay put.
2. Look up body context (planet_class, settlement_pattern,
cultural_corridor, population, economic_role) from systems.db.
3. Build a short corridor-aware few-shot prompt per feature type.
Prompts carry 3 concrete `Style: X. Answer: Y` examples so
Gemma 2 2B completes a pattern instead of generating to an
open-ended instruction — this is the single biggest lever
against placeholder echoes on a small model.
4. Stream the prompt into a long-lived sr-voice subprocess, read
the JSONL response, post-process (strip markdown, label
prefixes, brackets, reject 5+ word outputs and placeholder
tokens), check the earth-name blocklist, check per-(corridor,
feature_type) + per-body dedup, check the per-stem cap, retry
up to 3 times with a bumped seed.
5. On persistent failure, fall back to a deterministic palette
generator so every feature ends up with a name.
6. Write markers.json atomically and refresh atlas_* DB rows via
sync_markers_to_db. Commit the DB per body so a crash loses
at most one body of state.
7. Restart the sr-voice subprocess every `--refresh` requests
(default: 200) to prevent KV-cache context bleed.
Core design decisions:
- Determinism: per-(world_seed, body_id, feature_local_id, attempt)
seed so the full run is reproducible.
- Ordering: bodies are processed in ascending `hop_distance_from_gateway`
so core bodies get first pick at every unique Gemma output and
outer sectors fall into the palette fallback when they lose the
dedup race.
- Dedup scope: (cultural_corridor, feature_type) across the run,
PLUS a per-body cross-type set so the same name can't be a river
AND an ocean AND a mountain on the same world. Hand-authored names
are seeded into both sets on load so templates win priority.
- Stem cap: each non-generic root token (e.g. 'Arcturus', 'Meridian')
may appear at most `--stem-cap` times across the full run (default
20), preventing single-word runaway. Fallback names bypass the cap.
- Earth blocklist: 181 curated entries covering major Earth cities,
mountains, rivers, oceans, historical/colonial spellings, and
Greek/Roman mythology that reads too literally. Prefixed variants
('Nouveau Paris', 'New Tokyo') explicitly allowed per the product
intent that Earth-echo names are fine but must not dominate.
Leading 'The ' is stripped before comparison so 'The Great Divide'
also matches.
Operational features:
- `--shard N/M` slices the body list into M partitions for parallel
runs. Two terminals × `--shard 0/2` + `--shard 1/2` fits the
~2.5 GB/instance VRAM footprint twice under the 50% cap on a
16 GB AMD GPU and roughly halves wall time.
- `--log PATH` writes a timestamped tee of every status line to a
file. Default: `.tmp/gemma_naming.shard{N}of{M}.log` when a
non-trivial shard is in use.
- SQLite `PRAGMA journal_mode=WAL` + `busy_timeout=15000` so two
concurrent shards serialize writes without lock errors.
- Per-body progress lines report `body K/N`, `sys K/N`, and
`hop=H` so the user can watch core sectors finish first.
- Each body logs the new names it produced per feature type so the
user can eyeball quality as the run progresses.
- Checkpoint summary every 25 bodies: cumulative names, rate,
ETA — gives the log regular scroll points.
- `--mock` uses `server/sr-voice/mock-stdio.sh` for dry-fire
pipeline validation without a model load (tested end-to-end).
Supporting files:
- `tooling/planet-gen/earth_blocklist.txt` — 181 curated entries.
- `tooling/db/backfill_cultural_corridor.py` — one-off migration
that fills the `cultural_corridor` column on both `star_systems`
and `bodies` from the `geographic_sector` values. Before this
pass, 99.4% of rows (3221/3240) had a NULL cultural_corridor
despite `wiki_sync.py` being aware of the column — the wiki
index.md files only carry the sector header, which was never
propagated to the DB column. Idempotent, safe to re-run after
any wiki_sync rebuild, explicit transaction wrapper with
rollback on failure.
Full batch runtime estimate: ~20 hours single-shard / ~10 hours
double-shard on this hardware. Smoke tests across five hardened
iterations (v1–v5) on GJ71b/c/d/d-1/e confirm the pipeline produces
clean, varied, culturally-coherent names with zero post-processing
residue.
Remove redundant and stale patterns:
- content-ron/, client-tmp/ — vanished dirs not referenced anywhere
- tooling/planet-gen/__pycache__/, sol_data/__pycache__/ — already
covered by the global __pycache__/ pattern
- tooling/planet-gen/sol_data/.cache/ — already covered by .cache/
- db/commonwealth.db* — leftover guard from the pre-rename era;
the stale on-disk file has been deleted
Also rewrite the misleading "Runtime databases (not tracked — created
on startup)" comment: server/data/systems.db is actually tracked, so
what the block really ignores are WAL/SHM sidecars plus the local-only
settings.db.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both lint tools are used during pr-review pre-flight checks. Adding them
alongside the existing cargo check / test / build entries avoids
permission prompts during the review cycle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ON DELETE CASCADE added to every atlas_* foreign key (atlas_body_grids,
atlas_cities, atlas_roads, atlas_railroads, atlas_pois, atlas_rivers,
atlas_oceans, atlas_mountain_ranges). Previously, deleting a body from
the bodies table or NULL-ing its terrain_reference would leave orphan
atlas rows forever — sync_markers_to_db only cleans up for bodies it
re-processes. The existing atlas tables in systems.db were dropped and
recreated with the new constraint; FK list now reports CASCADE.
- Atlas DDL deduplicated. systems-schema.sql is now the single source of
truth, bracketed by `-- BEGIN ATLAS INDEX` / `-- END ATLAS INDEX`
markers. generate_atlas.py reads that block via `_load_atlas_schema()`
and applies it at runtime, so there is no second copy of the DDL to
keep in sync. Adding a column requires one edit, not two.
- Uniqueness guard on city coordinates. `_enforce_unique_city_coords`
runs at the end of `place_cities` and deterministically perturbs any
duplicate (row, col) via a fixed spiral walk to the first free
walkable land cell. Rare in practice but the MST collapses to a
zero-distance edge otherwise, producing an empty A* path and silently
dropping the road.
- Grid header validation. `load_markers` now raises `AtlasGridMismatch`
if the loaded `grid: {w, h}` header does not match `GRID_W`/`GRID_H`.
Both the incremental-skip path and the regenerate path route through
this loader, so a hand-authored template shipping a different grid
size fails loud with a per-body error rather than silently producing
half-scale coordinates.
- Unused `seed_rng` parameter removed from `_analyse_terrain`. The
function is RNG-free (continent flood-fill, habitability scoring,
river-mouth dedup, cost grid — all pure functions of terrain). The
false API contract made it look like terrain analysis consumed RNG
state and had to be sequenced with downstream RNG use.
- `_score_capital_sites` river-mouth bonus now builds one sparse
accumulator with all mouth points set at once and runs a single
`gaussian_filter` call, instead of O(n_mouths) filter calls over
single-point images.
- `binary_dilation(analysis["land_mask"] == False)` replaced with the
idiomatic `~analysis["land_mask"]`, matching the convention used
elsewhere in the file.
- `atlas-generate` Makefile target now guards on
`SELECT COUNT(*) FROM bodies WHERE terrain_reference IS NOT NULL`.
On a fresh DB that count is 0 and the generator previously exited
"success" after processing zero bodies. The target now fails loud
with a pointer to `populate_terrain_reference.py`.
- `main.rs` SimRng defensive re-insertion gains a long comment
explaining the exact plugin-ordering hazard it guards against, so
future readers don't treat the line as dead code. Tied to #826.
- V-B06 enum validation: the five VALID_* sets
(VALID_BRAND_CATEGORIES, VALID_VALUE_TRAJECTORIES, VALID_SCARCITY_CLASSES,
VALID_BRAND_TIERS, VALID_CURRENCY_DENOMINATIONS) were defined but never
referenced. brand_products.brand_category etc. are plain TEXT with no
CHECK constraints, so a typo like `brand_category = "terrior"` silently
imported. `validate_brands` now runs a V-B06 pass that asserts every
enum column is a member of its VALID_* set. V-B01..V-B05 + V-B06 all
reported together on import failure.
- Explicit transaction wrapper: the clear-then-reimport cycle (10 DELETEs
followed by 9 imports and structural validation) used to depend on
Python's implicit-deferred-transaction semantics and sys.exit() on
validation failure. A crash mid-import could leave the DB with some
tables empty and others intact. The body now runs inside
`conn.execute("BEGIN")` + try/except with an explicit `_ImportAborted`
for validation failures and a `BaseException` catch-all for
KeyboardInterrupt / programmer errors. All failure paths rollback
before exit; the commit only fires after structural validation
passes. Dry-run leaves the transaction open so the coverage check
below can still SELECT against in-memory state.
- system_fiscal docstring: previously cited the D-189 §6 derived
formula (`collection_efficiency = 1.0 - shadow_economy_intensity × 0.6`)
while the implementation hardcodes `collection_efficiency = 0.85` for
every system. The docstring now explicitly states these are Phase 2
placeholder values (with named constants PHASE2_CORP_TAX_RATE and
PHASE2_COLLECTION_EFFICIENCY) and calls out the shadow_economy.toml
pipeline as the Phase 3 follow-up.
Blocking PR #129 items 1, 2 (plus polish 16 and 17):
- D-191 §8 prose rewritten to match the code. The previous amendment said
positions were `{x, y}` objects against a "typically 1024 × 512" grid,
but the generator, the six hand-authored templates, and all 2394
procedural seed files ship `[row, col]` integer arrays against a
`{"w": 512, "h": 256}` grid. The decision doc is now aligned with
reality: positions are `[row, col]`, the storage grid is 512 × 256,
and the row-first ordering is called out explicitly so readers can
cross-reference NumPy/flood-fill/A*/cost-grid conventions.
- §8 now follows the D-094 amendment pattern. The superseded 2026-04-10
prose is preserved verbatim as "Original (superseded)" with a dated
Amendment block on top — future readers can see what changed and why
instead of silently losing the history.
- brands.toml header gains a short Phase 2 boundary note. The 4 anchor
brands come from D-189 §5; the additional ~23 brands from D-189 §11
are deliberately deferred to Phase 3 — Phase 2 only needs the demand-
node plumbing and V-B01..V-B06 validation exercised end-to-end.
- systems-schema.sql `bodies.terrain_reference` comment now pins the
repo-root-relative path convention (wiki/star-systems/<slug>/bodies/
<body_id>/heightmap.png) so the three downstream pipelines (populate,
atlas generator, client loader) share a documented contract instead
of drifting against an unwritten convention.
10. Dropped the "no body data" fallback string in the picker panel — the
generator always writes entry["bodies"], so the fallback was dead. Use
the "—" convention the rest of the panel already follows.
12. Removed the autoload parse-order comment from AtlasPanel (it is
scene-instanced, not an autoload, so the rule does not apply), and
collapsed _build_heightmap_viewer to a direct AtlasViewer.new() —
mirroring the rest of the file rather than dancing around a risk that
is not real for this class.
13. AtlasMarkerOverlay._draw_cities now compares hover/selection by a
stable _city_key() (name → city_id → pos → hash) instead of
Dictionary.==, which was O(fields) per city per redraw. Preps the
renderer for much larger city counts without a rewrite.
14. Orbital click/draw handlers guard against missing body_id /
station_id by reading through str(dict.get(..., "")) and skipping
empty ids. Matches the defensive style already used for parent_body_id
and keeps a NULL id from crashing _draw_bodies / _handle_orbital_click.
4. AtlasPanel and AtlasViewer now compose their title + hint from an
ImplantHeader child rather than hand-rolling them via draw_string, so the
D-169 "theme swap changes the implant hardware appearance" invariant
holds end-to-end. _refresh_screen_header() drives content per level and
on system navigation.
5. AtlasViewer exposes city_canvas_pos(), get_hovered_city(),
get_selected_city(), and get_overlay_defs() as public API — the marker
overlay no longer reaches into underscore-prefixed state, which is
especially important because viewer is an untyped var in the overlay.
6. KEY_N now consumes unconditionally while the viewer is visible, and
main.gd's global economics-monitor toggle is gated on
!HudGroups.is_app_active("implant/map/atlas"). Previously pressing N
without a selected city fell through and closed the fullscreen atlas as
a side effect.
7. OVERLAY_DEFS lives in AtlasViewer as the single source of truth.
AtlasOverlayBar reads the list via viewer.get_overlay_defs(), and
AtlasViewer derives _overlay_visibility / _overlay_locked from the same
table at _ready() — no more hand-maintained parallel lists, so the bar
and the guard in set_overlay_visible can't drift.
8. AtlasOverlayBar drops `class_name`: it now loads via
load("res://ui/implant/atlas_overlay_bar.gd") from AtlasViewer, the same
pattern AtlasPanel uses for AtlasViewer. _init(viewer_ref = null) keeps
the required-arg footgun off the editor's introspection path.
9. `star-map-data` make target added to regenerate
client/data/star_map_data.json from systems.db + wiki, and
`check-star-map` wired into pre-pr-validate + pre-pr-client so any
commit that touches the generator (or any downstream systems.db change
like server #839) fails pre-pr until the JSON is regenerated. The
terrain_reference data-availability dependency is no longer tribal
knowledge.
Also addresses review #15 (push_warning on unknown overlay id in
set_overlay_visible) and #16 (disabled always-on buttons drop handler
churn) as part of the same refactor.
1. Moon placement on the orbital diagram divided by a hard-coded 4 — a gas
giant with five or more moons would overlap satellites and make them
unclickable. Count moons per parent and distribute them evenly.
2. AtlasViewer._load_markers seeded _grid_w/_grid_h from _tex_w/_tex_h
before the terrain_reference null check, so opening a body with no
heightmap after one that had a heightmap left the grid dimensions
pointing at the previous texture and misplaced markers. Reset texture +
grid dims to sentinel defaults at the top of _load_heightmap.
3. AtlasMarkerOverlay's three toggleable overlays (production_zones,
shadow_economy, corp_presence) read markers.production_zones /
markers.shadow_zones / markers.corp_presence — keys that don't exist in
D-191 §8's markers schema, so toggling was a silent no-op. Derive them
from cities[] instead: primary_function ∈ PRODUCTION_FUNCTIONS for
production, absent Commission presence for shadow bands, Commission
presence for corp dots. Follow-up ticket will formalise per-overlay
arrays once the server schema lands.
Addresses PR #128 review blockers 1-3.
Implements the Phase 3 atlas content generator per D-191 §3, §8, and §9.
Pipeline per body (terrain-aware, deterministic per seed + body):
1. Simulate terrain via planet_simulation.simulate().
2. Analyse continents (flood-fill), habitability (temp/moisture/slope +
coastal bonus), river mouths, and a terrain A* cost grid.
3. Place cities sequentially — capital first (habitability + river-mouth
bias), then corridor growth via multi-source Dijkstra, quadrant-spread
penalty after 2 cities in a quadrant, port-on-new-continent bonus at
cities 3–4. ±25% noise for seed variation.
4. Generate roads and railroads as an MST over city positions, with
A* paths on the terrain cost grid (rail follows roads where possible).
5. Place a transit POI at the capital (15% chance to scatter to a
secondary city).
Output (canonical markers.json schema, pixel space per D-191 §8):
- cities: {id, name, kind, center:[r,c], population}
- roads: {id, name, kind, path:[[r,c],...]}
- railroads: {id, name, kind, path:[[r,c],...]}
- pois: {id, name, kind, center:[r,c]}
- existing rivers/oceans/mountain_ranges preserved untouched.
City names are left empty for gemma_naming.py (#833). Body population is
split across cities with geometric decay (capital ~50%, each subsequent
city half the previous). The 6 hand-authored bodies (Lendel, Edict,
Vuurkloof, Røros, Cairnside, Estrade) are detected by existing
`cities` and skipped for regeneration; their markers are still synced
to the DB index below.
Atlas index in systems.db (new):
- atlas_body_grids, atlas_cities, atlas_roads, atlas_railroads,
atlas_pois, atlas_rivers, atlas_oceans, atlas_mountain_ranges
- Scalar metadata mirror of every markers.json — the implant atlas app
and development queries can lookup cities/POIs/features without
scanning 267 JSON files. Polyline geometry stays in the markers.json
files next to the heightmaps (used by the renderer); the DB only
stores filterable scalar fields plus `point_count` as a length proxy.
- Schema lives in server/data/systems-schema.sql; generate_atlas.py
mirrors the CREATE TABLE IF NOT EXISTS block so it runs against any
DB state (matches the economy-db importer pattern).
- Populated and refreshed on every run. Each body's rows are deleted
and reinserted deterministically — no stale state.
Also fixes a pre-existing WIP bug in the quadrant-saturation penalty
loop (a stray outer `for r in range(GRID_H)` with unreachable breaks
meant only the NW quadrant was ever checked).
Runtime: 280s for all 267 inhabited bodies on a single core. 265 bodies
updated this run, 6 hand-authored bodies synced to DB without
regeneration.
Atlas index after run:
atlas_cities 329 (15 hand-authored + 314 awaiting #833)
atlas_roads 46
atlas_railroads 44
atlas_pois 287
atlas_rivers 2034
atlas_oceans 696
atlas_mountain_ranges 1953
atlas_body_grids 267
AtlasOverlayBar is an HBoxContainer docked top-right of the heightmap viewer
with 11 short-label buttons mapping to D-191 §7 / D-181 signal visibility:
always-on (5) TER INF NAM GAT POL — terrain, infrastructure, named
features, gate markers, political zones. Pinned on; clicks
are swallowed so the layers can't accidentally be disabled.
toggleable (4) POP PRD SHD CRP — population density, production zones,
shadow economy, corporate presence. Reflect and mutate
viewer overlay state.
locked (2) STK BSL — stockpile_weeks, production_vs_baseline. Disabled
and greyed out with unlock-requirement tooltips, per D-181
semi-private/private tiers; kept in the bar so players see
that deeper data exists and is gated.
Each button writes through AtlasViewer.set_overlay_visible(), which is the
single entry point into the viewer's _overlay_visibility dict consumed by
AtlasMarkerOverlay._draw(). Locked overlays short-circuit in that setter.
Per D-191 criterion 6.
Adds Level.HEIGHTMAP_VIEWER to AtlasPanel. The viewer loads a body's
terrain_reference heightmap PNG, pairs it with markers.json (roads, rail, POIs,
cities, rivers/oceans/mountains), and renders markers in texture-space via an
AtlasMarkerOverlay Node2D child of a transformed canvas — pan = offset,
zoom = scale.
Pan/zoom is cursor-centred (wheel zooms under the mouse, drag pans), with a
fit-to-view reset on R. Empty markers.json state renders a bare heightmap;
missing terrain_reference shows a themed "terrain data pending (#839)" notice
instead of crashing.
City data sidebar rebuilds from the selected city: name, pop tier, function,
currency zone, Commission presence, shadow zone, gate distance. Pressing N on
a selected city emits economics_link_requested(system_id) — main.gd bridges
this to EconomicsPanel.select_system() + HudGroups.open_app("implant/economics")
as an insert overlay, satisfying the D-191 Phase 2/3 cross-panel integration.
The overlay renders all nine D-191 overlay layers off of per-overlay visibility
flags in AtlasViewer. Overlay toggling for the regional view (#836) plugs into
set_overlay_visible(); the five always-on layers (terrain, infrastructure,
named features, gate markers, political zones) draw by default, the four
toggleable layers draw from placeholder data, and the two locked layers
(stockpile_weeks, production_vs_baseline) remain off until unlocked.
Per D-191 criteria 1, 4, 5.
The generator and the hand-authored templates (Edict, Vuurkloof, Røros,
Cairnside, Estrade) already store markers in heightmap pixel space with
a grid header. Update §8 to match: {x, y} integer pixels are the storage
format, and lat/lon strings become a display-time derivation in the
atlas UI (synthesized from position + grid dimensions + body radius).
Avoids double-conversion through an equirectangular projection and keeps
the hand-authored markers.json files as-is.
Adds tooling/planet-gen/populate_terrain_reference.py and runs it against
systems.db. Resolves each body's expected wiki heightmap path (repo-root
relative) and writes it into bodies.terrain_reference. Missing heightmaps
are logged for remediation.
Result: 2380/3240 bodies populated, 860 still missing heightmaps. This
unblocks generate_atlas.py (#832) for every body that has a heightmap.
Adds the brand layer per D-189 §5:
- Schema: brand_products, brand_inputs, system_fiscal, corp_financial_state,
corp_lifecycle_events (+ 5 indexes).
- Importer: reads wiki/economics/corporations/brands.toml, populates the
new tables, validates V-B01–V-B05 structural rules, and derives
system_fiscal for inhabited systems.
- Data: 8 brand_products, 16 brand_inputs, 301 system_fiscal rows.
Brand products are demand nodes — they consume commodities; they are not
commodities themselves (D-185). Depends on copy PR #127 for the corp
records referenced by brands.toml.
3-level atlas navigation as an implant/map/atlas FULLSCREEN app (D-170 z=20):
system picker (◄ ► cycle, Enter opens orbital) → orbital diagram (star centre,
bodies grouped by orbit_index with moons sub-orbiting parents, stations as
markers, click-to-open body or mini station panel) → body entry (data sheet,
Enter stub for #835 heightmap viewer, Esc back). Composed from the ImplantPanel
component library (D-169).
Wires the panel into hud.tscn, adds an A-key toggle in main.gd, propagates
insert_state via snapshot_consumers. Extends generate-star-map-data.py to emit
per-system orbit_bodies + stations arrays (with currency_zone, atmosphere,
population, terrain_reference, etc.) from systems.db; star_map_data.json
regenerated deterministically.
Per D-191 §6, Phase 3.
Replaces the hardcoded seed=0 with the seed received in StartupMessage,
threading it through SimulationPlugin -> EconomyPlugin / SimRng. Integration
test fixtures updated for the new SimulationPlugin { seed } signature.