Files
jpmschweitzerandClaude Opus 5 cb5d3f1335 refactor(config): T-1281 — the check domain retires its five scripts
All five gates are ported, tested against their failure paths, and the
originals are gone. reach check is the only way to run them.

Parity first, then deletion. Every case in test_check.py began as a parity case
running the new implementation beside the script it replaced; that evidence is
in the ticket. With the scripts retired there is nothing left to compare
against, so the assertions become the spec and the file drops its "_parity"
name. A parity test is scaffolding with a defined lifetime — keeping one after
its subject is deleted would mean keeping the subject alive to be compared
with, which is the opposite of a migration.

Two gates could not be parity-tested in a fixture at all, and both reasons are
findings rather than obstacles. canvas-version: canvas_sources globs from a
__file__ root while the service resolves git through config.repo_root(), so a
fixture would diff one tree and glob another — real history is used instead,
including two genuine instances of the regression the gate exists to catch.
systems-db-stamp: generator_sources raises at IMPORT time when the economy-db
tree is absent, so the old script died before reaching any logic in every
fixture. The ported service imports it lazily and after the absent/unstamped
checks, which is exactly why those states are testable now and were not before.

Hooks rewired: pre-commit runs reach check fact-ids, pre-push runs the other
four. Both pass --no-input, because a hook has no TTY and a prompt there does
not wait, it crashes. Both guard on `command -v reach` and skip with a message
rather than blocking every commit on a missing tool.

Make targets are RETIRED, not wrapped, per the D-263 split — with the mapping
left as a comment where they used to be. Wrapping would leave two ways to
invoke each gate, and reach --help would stop being the answer to "what tooling
exists" while the Makefile remained a competing index. pre-pr-validate and
pre-pr-content keep their orchestration role and lose the individual target.

Sprint archives and workshop notes still name the old paths and are left alone:
they record what was true when written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:33 +02:00

186 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The Settled Reach
A top-down life-sim — asymmetric information, occlusion-based perception, single-character perspective, Rimworld-style storyteller. Systems interactions like The Sims, combat and visuals like single-character Rimworld, world generation inheriting patterns from Dwarf Fortress and NMS, economy inspired by X4/EVE. Many systems interactable and ready for player participation, but capable of being ignored — the world is alive for any given run. Set in an original science fiction universe. Godot 4 client + Rust/bevy_ecs simulation server via subprocess/IPC.
**Official Title:** The Settled Reach
**Repository name:** settled-reach
**Version source of truth:** `project.yaml` (root `version` field, scheme: `0.{phase}.{n}`), mirrored into `client/project.godot`'s `application/config/version` — the client can't read `project.yaml` at runtime (an exported build has no repo root), and the Atlas disk cache keys its only invalidation signal on that value. Bump both; `reach check client-version` (run by the pre-push hook) fails on drift. See `client/scripts/build_version.gd` (T-1241). **Changing canvas generation requires bumping this version** — otherwise warm caches keep serving canvases built by code that no longer exists (five regressions, most recently T-1239). The pre-push hook enforces it via `reach check canvas-version` against the path registry in `tooling/canvas_sources.py` (T-1242); there is no override, and bumping when unsure costs one cache miss.
## Project Structure
```
wiki/ # THE SEED — authored world content that the generator varies from
client/ # Godot 4 client
server/ # Rust/bevy_ecs simulation server
tooling/ # Build tools, scripts, asset pipelines
tests/ # Integration and end-to-end tests
docs/ # Architecture, design, briefings, workshops
db/ # Schema + seed data (connectors moved to tooling/db/)
.claude/ # Agents, skills, rules
governance/ # Decision records — decisions/ questions/ rejected/ per domain (pql DQR tree)
```
Full annotated tree: `.claude/rules/project-structure.md`
**`wiki/` is the seed, and it is mostly generated.** 11,864 files — the largest
tree in the repo. Two ownership models run in OPPOSITE directions, and mixing
them up costs work: `star-systems/` (3,262 pages, 93% of the wiki) is produced
FROM `systems.db` by `tooling/db/wiki_sync.py`, so its `<!-- READ-ONLY -->`
sections and body-page frontmatter are OUTPUT — hand-edits are silently reverted
on the next sync. The flat categories (`corporations/`, `factions/`, `economics/`,
…, 253 pages) are INPUT the generators read. Body frontmatter IS the body
definition (`seed`, `land_fraction`, `axial_tilt_deg`); `wiki/economics/` seeds
world content generation, not just DB rows. Read `Skill(wiki)` before touching
any of it — the wiki documents itself well (`wiki/GOVERNANCE.md`,
`wiki/_templates/`, `wiki/authoring/`, `wiki/economics/schema.md`), it was simply
unreachable from here until 2026-08-20.
## DevOps
See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. All development operations go through the top-level `Makefile` — run `make` for a summary of targets.
### Asset pipeline
`server/data/systems.db` is a read-only canonical snapshot produced by a single
generator (`import_economics`, which runs `generate_brands` internally) — never edit
it directly. To regenerate: `make regen-db`. Full rules in
`.claude/rules/asset-pipeline.md`.
## Development Cascade — First Things First
Development follows a strict cascade. Each phase has a concrete deliverable. **Do NOT discuss, design, or implement detail from a later phase while an earlier phase is incomplete.** If you encounter references to later-phase detail (room grammar, NPC bundles, heritage tokens, etc.) in documents or decisions, either ignore them silently and stay at the correct level, or flag that the reference is dragging attention to the wrong scope level and suggest it be rephrased or moved.
| Phase | Focus | Deliverable |
|-------|-------|-------------|
| 1 | Wiki content complete — all planets, moons, stations, heightmaps, artwork | Implant-ready Godot map of the Reach with click-throughs + wiki/GTTR popups |
| 2 | Economics layer — supply/demand, transport, political/social pressure, corporations, supply chains | Economics spreadsheets/graphs with runtime-tweakable simulation |
| 3 | Planetary/moon maps & station layouts — cities, rivers, mountains, roads, biomes, rail | Atlas of the Reach (implant app) |
| 4 | World generation (tile/chunk/block) — deterministic multilayer seed→tile cascade; each layer above the tile fill viewed as a **map in the implant Atlas** (no in-world rendering; tile/voxel output is harness-verified, never Atlas-mapped; **quarter (512 m) is the deepest Atlas rung** — D-255 amended 2026-08-07, superseding the chunk floor: the deepest rung is the one where a screen **pixel shows one subtile**, and below that a rung magnifies rather than informs. **Chunk stays the 64 m stream/derive unit and is vital for Phase-5 first-person content — only its Atlas *viewability* was retired**) | Deterministic walkable-world data + per-layer Atlas maps + asset catalog + the **stepped Atlas ladder** (D-255, amended 2026-07-26 and 2026-08-07 — the extent inversion: four levels — the Global body-surface opener (rung 0, viewport-sized, always kept) + three fixed rungs Region→District→Quarter; **a rung fixes the canvas EXTENT and spacing falls out of it** — the shorter viewport axis spans exactly one cell of the rung's level, so canvas cost is constant at every rung — drawn texel-exact by the client map-art function at a uniform 2×2 display ratio; heightmap PNGs remain invisible derivation input, never displayed. Rung 0.5, an expanded whole-body layer where hydrology resolves once and every rung below sources from it, is RULED (D-258) but not yet implemented — see D-258) |
| 5 | Player control & in-world rendering — character, walls/stairs/doors, lighting, drawn on **generated** tiles (no test map) | Player viewport with final-version assets on the generated world |
| 6 | Detail coloring — room-level NPC population, cultural room grammar | Only when the world is walkable |
**v0.2 target is dropped.** No scoping negotiations. Build the base systems fully.
**Generation before player (D-166, amended 2026-05-22 and 2026-06-12).** The world is built **outside-in**: geo layer → economic layer drawn onto the world (cities, buildings, streets, blocks, inter-settlement roads/rail) → building templates → **door boundary contracts** to building insides → only then the background NPC layer + interiors (Phase 6) → only then gameplay/scenarios ("making it a game again", parked in epic T-1036). Player control and visual rendering (Phase 5) ramp **in parallel once outside generation produces walkable exteriors** (gate held by T-962) — not before. There is **no hand-made test map** — test layers are produced by the generator itself. Progress is viewed through the implant **Atlas** (per-layer maps), which already exists (Phase 3). The current in-world rendering code is **left as-is until Phase 5** — do not build on it or rip it out before then. Guiding statement: *before this is a game, it is a Reach a character can travel through.* Phase-independent maintenance/test debt lives under initiative T-1037, not a phase epic.
## Spatial scale ladder (D-243)
The generation cascade is a **nested, absolute-metre containment ladder** with **exactly one elastic seam** — the jump to planetary scale. Every level below the seam is a fixed integer multiple of the one below, identical on every body; only the planetary count floats per body. Full detail + rationale: **[D-243](governance/decisions/architecture.md#d-243)**.
| Level | Side | = | Role |
|-------|------|---|------|
| voxel | 1 m | — | the tile |
| chunk | 64 m | 64 voxels | stream / derive unit |
| block | 128 m | 2 chunks | generator planning unit |
| quarter | 512 m | 4 blocks | settlement footprint cell |
| district | 2,048 m | 4 quarters | urban division **and** local-climate cell |
| **region** | **~205 km** | 100 districts | **top hard block** — climate/weather/season lockdown + planetary grid |
| *— elastic seam —* | | | |
| planet | per body | `round(2πR / 204.8 km)` regions | the **only** per-body-floating quantity |
- **Region** is the largest *hard* block: the unit where **weather, season, and the temperature baseline are resolved** (the Q-105 cheap-dynamism source — computed once, inherited by every district/tile inside). It is also the sane planetary grid (~195×98 regions on an Earth-sized body).
- **Climate is edge-fuzzed** — the region/district is where climate is *computed*, never where it *steps*. Sampled values are a warp-perturbed blend across neighbours, so the grid is invisible (*climate does not change on a line*). Terrain detail finer than the heightmap (~4078 km/px) is **invented deterministically** (interpolation + domain warp + detail-scatter, D-227), never stored.
- **Vocabulary (locked):** "region" = the ~205 km metric cell **only**. The old 1 km "RegionProfile" is dropped — its terrain/climate carrier role lives on the **district**. **Province** (D-201 tier-4, 50500 km watershed/political) is an irregular lore-bearing **overlay** painted across regions — *not* a containment rung; keep it distinct from "region". `sector` is reserved (`perception::VisibilitySector`); do not reuse it for scale.
## Work Modes
### Kanban mode (default)
Work flows continuously through the current phase. `/whats-next` selects the next epic-sized batch from the dependency graph (`pql ticket list --under <active-phase-epic> --unblocked`), refines ticket context via parallel Si agents, then activates a worktree. `/pr-process` (from worktree) and `/pr-review` (from main) handle the review cycle through Gitea PR comments. Phase gating is the **cascade hierarchy** (initiative `T-745` → six phase epics), not labels or a milestone entity — a ticket is in a phase by being parented under that phase epic. The active phase is the `in_progress` phase epic (sequential per D-166; currently `T-750`, Phase 4).
### Pair session
Human and Claude work together interactively on a single task. No background agents, no autonomous work. Used for load-bearing architecture changes where the human needs to make judgment calls as the work progresses — not approve a finished result.
**Rules:**
- No `Agent` spawns, no `run_in_background`. One thread of work.
- Propose one change at a time. Wait for human reaction before continuing.
- Explain what you're about to do and why before doing it.
- After each change, verify together (compile, test, inspect) before moving to the next.
- The human is a participant, not a reviewer. Ask questions, surface tradeoffs, flag risks in real-time.
**When to use:** Ticket description says "pair session", or the work touches foundational systems where a wrong call is expensive to undo (tick cycle architecture, protocol design, data model migrations, build pipeline rewrites).
## Agent Instructions
### Agent scope
- All file paths are relative to the repo root (e.g. `server/src/bridge/types.rs`).
- Topic branches per logical work unit (e.g. `heightmap-import`, `city-names-table`). Any agent can touch any file — personalities guide perspective, not file access.
- **Never chain git commands** in a single Bash call (e.g. `git add ... && git commit ...`). Always run `git add` and `git commit` as **separate sequential Bash calls**.
- **Keep Bash commands short and simple** — one operation per call. The permission gate rejects overly complex commands ("Denied — too complex for the permission system to approve cleanly"): avoid long pipelines, multiple `;`/`&&`-chained commands, nested `$()` subshells, and intricate `grep -oE`/`python -c` one-liners in a single call. Prefer a dedicated tool (Read, etc.) where one fits, or split the work into granular steps / a small script file.
- **Stale git lock files:** If a `git` command fails with `index.lock: File exists`, you may remove the lock file at `.git/index.lock` (or `.git/worktrees/<name>/index.lock` if in a worktree).
### Planning store (pql)
Tickets live in `.pql/pql.db`, rebuilt from the **git-tracked** `.pql/changelog/` (no
binary-DB backup ritual). Decisions are markdown-sourced under
`governance/{decisions,questions,rejected}/` and synced into the same DB. Ticket ids are
`T-NNN` (`T-440 == legacy #440`). The pre-commit hook auto-exports + stages the changelog.
**This is a pql repo — read the `pql` skill (`Skill(pql)`) before any *non-trivial*
planning-store operation**, not just routine ticket/decision CLI. The rules here cover
day-to-day `pql ticket`/`pql decisions`; the load-bearing model that bites in edge cases
lives only in the skill: mutations **write through** to `.pql/changelog/` synchronously
(so **never `git restore`/hand-edit the changelog** — it is data, not a cache), `pql plan
export` does **not** replicate `ticket_idmap` (so it can't regenerate ids you discarded),
and **post-checkout/post-rewrite rebuild `pql.db` from the changelog**. Therefore moving
planning across branches means **re-creating via `pql ticket`**, not changelog surgery;
back up `.pql/pql.db` before any risky planning-store op.
### Before starting work
1. Check active work: `pql ticket list --status in_progress`
2. Check your ticket: `pql ticket show T-<id> --with-context`
3. Check dependencies: `pql ticket show T-<id> --with-blockers`
4. Read relevant `governance/**/*.md` decision record(s) referenced in the ticket (`pql decisions show <id>`)
5. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
### CLI tools
**Use `pql`** for tickets and decisions. Never use the `sqlite3` CLI — it crashes in Claude Code (std::bad_alloc).
| Tool | Command | Full reference |
|------|---------|----------------|
| Tickets | `pql ticket list`, `show`, `new`, `status`, `assign`, `label` | `.claude/rules/ticket-cli.md`, `/ticket` skill |
| Planning | `pql plan whatsnext`, `review`, `status`, `board` | `.claude/rules/ticket-cli.md` |
| Decisions | `pql decisions show`, `list`, `claim`, `validate`, `sync` | `.claude/rules/ticket-cli.md` |
| Vault query | `pql query`, `search`, `backlinks`, `related`, `context` | — |
Phase gating is the **cascade hierarchy** (initiative `T-745` → six phase epics), not a milestone entity: `pql ticket list --under <active-phase-epic> --unblocked` (active phase = the `in_progress` phase epic, currently `T-750`).
### Testing preferences
- **Prefer live Gauntlet testing over mocks.** For visual tests and rendering verification, use the full client/server pipeline (`--test-mode` + `SR_LIVE=1`) instead of TestHarness mocks. The Gauntlet test world produces production-identical data. Mocks can mask rendering bugs by taking different code paths.
- **Gauntlet rooms are immutable.** Never modify existing rooms — new systems get new rooms. This ensures StableId determinism and fixture stability.
- Three test tiers: (1) Live server — highest fidelity, (2) MessagePack replay via `Protocol.decode_snapshot()` — for unreachable rooms, (3) TestHarness mock — for UI-only tests where fog data doesn't matter.
- `make fixtures-gauntlet` regenerates real server snapshot fixtures from the Gauntlet world.
### Implant UI component library (D-169, D-170)
The implant UI system — all diegetic neural overlay panels — lives at `client/ui/implant/`. Components are Godot Control scenes styled via a shared `Theme` resource (`default_implant.tres`). Do not hand-roll implant panel layouts; compose from the library.
**Components:** `ImplantPanel` (root container), `ImplantHeader` (title + subtitle), `ImplantSeparator` (horizontal rule), `ImplantDataRow` (key/value row, optional color), `ImplantTextBlock` (RichTextLabel for wrapping text).
**Theme resource:** `client/ui/implant/default_implant.tres` — defines semantic color roles (`PRIMARY_TEXT`, `DIM_TEXT`, `ACCENT_ACTIVE`, `ACCENT_POSITIVE`, `ACCENT_NEGATIVE`, `ACCENT_WARNING`, `SEPARATOR`), spacing, and font sizes. Swap the entire `.tres` to change implant hardware appearance at runtime.
**HUD visibility (D-170):** `client/scripts/autoloads/hud_groups.gd` manages z-index layering. Modes: `GAMEPLAY` (z=0), `INSERT` (z=10), `FULLSCREEN` (z=20), `MODAL` (z=30). App paths are hierarchical: `implant/map`, `implant/wiki/gttr`, etc. Opening any `implant/*` app occludes gameplay; closing returns to gameplay. Key API: `open_app()`, `close_app()`, `toggle_app()`, `is_app_active()`. Emits `gameplay_occluded` signal so renderers can pause.
**GameplayRenderer base class:** `client/scripts/rendering/gameplay_renderer.gd` — extends `Node2D`. Subclasses override `_gameplay_process()` and `_gameplay_draw()`. Connected to `HudGroups.gameplay_occluded` to pause when the implant is fullscreen. Used by `CursorRenderer`, `EntityRenderer`, `FogEntities`, `SoundIndicatorRenderer`, `WorldRenderer`.
### GDScript conventions
**Autoload parse-order rule:** Autoload scripts (`client/scripts/autoloads/`) compile before global `class_name` scripts are registered. Referencing a `class_name` type directly in an autoload causes a parse-time "not declared" error. Pattern:
- Declare fields untyped: `var my_field = null` (comment the intended type)
- Do **not** reference `class_name` types at the top level or in `_ready()` of autoloads
- In method bodies called at runtime (e.g. `apply_snapshot`), use `load()` inline — by then the script is cached and `load()` returns the cached resource without reloading: `var CVD := load("res://scripts/rendering/character_visual_descriptor.gd")`
- Do **not** cache the `load()` result in `_ready()``_ready()` fires during autoload init, before the target script is in the resource cache, causing an actual file reload that breaks self-references in scripts using their own `class_name`
`game_state.gd` (`character_visual_descriptor` field) and `sim_bridge.gd` (`harness` field) follow this pattern.
### File conventions
- Decisions: records in `governance/{decisions,questions,rejected}/<domain>.md` (see `governance/README.md`; query via `pql decisions`)
- Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected)
- **Claim IDs before writing:** `pql decisions claim D <domain> "title"` — prevents ID collisions across parallel branches
- Diagrams: `.d2` source + `.svg` renders in `docs/diagrams/{category}/``make diagrams` renders, `make check-diagrams` gates, `clide draw --file <path>` shows one (it takes `.d2` source directly). Create or update diagrams when D-records are added or modified, via `Skill(d2-diagram)` plus `.claude/rules/diagrams.md`.
- Discussion rounds: numbered sequentially, archived to `docs/discussions/` when complete
- Briefings: one per agent, updated after decision-producing rounds
- Tickets: managed via `pql ticket`/`pql plan` CLI or `/ticket` skill (`T-NNN` ids)