Hoshe (QA): docs/DEVOPS.md Repository Layout still listed `decisions/` — corrected to `governance/` (the DQR tree) and added a `.pql/` entry for the planning store. Tyre (architecture, non-blocking): tooling/db/common.py docstring named deleted scripts as consumers and `resolve_db_path`/`load_config`/`get_connection` were dead settledreach.db code. Trimmed common.py to just `ensure_venv` (the only symbol any kept connector imports) and rewrote the docstring to name the real consumers. ruff clean; common.py parses; ensure_venv intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
title, description, type, status
| title | description | type | status |
|---|---|---|---|
| DevOps Procedures | Build, test, lint, and CI procedures for the Settled Reach project — Makefile targets, worktree setup, server/client builds | architecture | active |
DevOps Procedures
Operational procedures for building, testing, and running The Settled Reach.
Repository Layout
client/ Godot 4 client (GDScript, scenes, assets)
server/ Rust/bevy_ecs simulation server
tooling/ Build tools, scripts, asset pipelines
tests/ Integration and end-to-end tests (cross-boundary)
governance/ Decision records — decisions/ questions/ rejected/ per domain (pql DQR tree)
.pql/ pql planning store — git-tracked changelog/ + config.yaml (pql.db is rebuildable)
.config/ Configuration files (linters, formatters, CI)
.cache/ Local caches for testing/linting (gitignored)
docs/ Design, architecture, briefings, workshops
db/ Schema + seed data (asset connectors at tooling/db/)
Unit tests live inside their respective projects (server/ uses #[cfg(test)] inline + tests/ directory per D-030). The top-level tests/ directory is for integration tests that cross the client-server boundary (IPC round-trip, serialization fixtures, divergence tests).
Prerequisites
| Tool | Version | Purpose |
|---|---|---|
| Rust (via rustup) | stable | Server compilation, clippy, rustfmt (auto-installed by make setup) |
| Godot | 4.x | Client editor and runtime (auto-installed to ~/bin/ by make setup) |
| Python | 3.x | Tooling scripts, db connectors |
| Make | any | Task runner (see below) |
| curl | any | Downloading Godot |
| unzip | any | Extracting Godot |
Makefile Targets
All development operations go through the top-level Makefile. Run make with no arguments for a summary.
Setup
make setup # Install/verify all dev dependencies
GODOT_VERSION=4.4 make setup # Pin a specific Godot version
Downloads and installs Godot to ~/bin/godot4, installs Rust clippy + rustfmt, and verifies Python/curl/unzip. Skips the download if the correct version is already installed. The GODOT_VERSION variable defaults to 4.6 and can be overridden.
Build
make build # Build both client and server
make build-server # cargo build in server/
make build-client # Client builds are editor-managed (prints guidance)
Run
make server # cargo run in server/
make client # Launch Godot with client/ project
The server must be running before the client connects (subprocess launch will be automated later per D-020).
Test
make test # Run all tests (test-server + test-client)
make test-server # Rust tests via tests/run-rust (cargo nextest, JSON summary)
make test-client # Godot tests via tests/run-godot (gdUnit4 headless, JSON summary)
The IPC test layers (D-030) have dedicated targets:
make test-ipc-fixtures # Layer 1: serialization round-trip fixtures
make test-ipc-protocol # Layer 2: mock LocalBridge protocol tests
make test-ipc-integration # Layer 3: real subprocess round-trip (+ benchmark when ready)
make test-ipc-benchmark # IPC latency benchmark (blocked: #555/#556 handshake)
Each tests/run-* script outputs a JSON summary to stdout and streams progress to stderr:
{"suite":"rust","total":42,"passed":42,"failed":0,"duration_ms":1230}
All scripts accept --filter <name> to run a subset of tests. They are whitelistable for agent use (no TTY prompts, no interactive input).
Server tests use Rust's built-in test framework with #[cfg(test)] inline tests and tests/ integration tests (D-030). Client tests use gdUnit4 (D-030).
Cross-Encoder Fixtures
make fixtures # Regenerate Rust->GDScript fixtures (server/tests/gen_fixtures.rs)
make fixtures-client # Generate GDScript->Rust fixtures + verify Rust decoder (#475)
The bidirectional protocol is validated by two sets of committed fixtures:
- Rust encodes, GDScript decodes:
client/tests/fixtures/msgpack/(generated bymake fixtures) - GDScript encodes, Rust decodes:
server/tests/fixtures/gdscript/(generated bymake fixtures-client)
Regenerate both after any protocol change. Commit the updated fixtures alongside the code change.
Troubleshooting fixture failures:
make fixtures-clientfails with encode errors: Check thatclient/addons/messagepack/messagepack.gdis up to date. The script exits non-zero on any encode failure.gdscript_generated_fixtures_deserializefails: Fixtures inserver/tests/fixtures/gdscript/are stale or corrupted. Re-runmake fixtures-clientand commit the updated files.- Fixture staleness in
make pre-pr: Protocol changed but fixtures were not regenerated. Runmake fixtures && make fixtures-client, then commit bothclient/tests/fixtures/andserver/tests/fixtures/gdscript/.
Golden File Management
make golden-diff # Show diff if golden file output has changed
make golden-update # Regenerate golden file and stage for commit
The golden file (server/tests/golden/proof_room_tick_10.json) is a committed snapshot of ObserverSnapshot output after a deterministic 10-tick replay. It catches unintentional changes to simulation output.
Workflow after intentional simulation changes:
- Run
make golden-diffto see what changed - Review the diff — confirm changes are expected
- Run
make golden-updateto regenerate and stage the new golden file - Commit the updated golden file alongside your simulation change
golden-diff exits 1 if the golden file has changed (useful in scripts). golden-update regenerates the file and runs git add but does not commit — the developer reviews and commits manually.
Lint
make lint # Run all linters
make lint-server # clippy (deny warnings) + rustfmt --check
make lint-client # gdlint/gdformat
CI (Local)
Run the full CI pipeline locally before pushing:
make ci # Both pipelines
make ci-server # lint-server → build-server → test-server
make ci-client # lint-client → build-client → test-client
CI targets chain lint → build → test sequentially. A failure in any stage stops the pipeline.
Pre-PR Checks
Before pushing a PR, run:
make pre-pr
This runs all checks in order: lint → build → test → content validation → fixture staleness. Total runtime ~2.5 minutes (incremental build), under 3 minutes clean.
For branch-specific checks:
make pre-pr-server # Server changes: lint, build, test, fixture staleness
make pre-pr-client # Client changes: lint, build, test
make pre-pr-content # Content changes: schema + cross-reference validation
If pre-pr-fixtures fails, your protocol changes require fixture regeneration:
make fixtures
git add client/tests/fixtures/
git commit -m "chore(fixtures): regenerate for protocol v8"
The fixture staleness check is a blocker (exit 1) — stale fixtures cause false positive client tests.
Clean
make clean # Remove build artifacts and .cache/ contents
Content Validation
make validate-content # Validate content YAML against JSON schemas
make check-fact-ids # Check fact_id references against knowledge catalogs
Gauntlet Checklists
make checklist-validate # Validate checklist YAML against schema (standalone)
make checklist-generate # Validate + print per-room condition summary
Checklists live at content/gauntlet/rooms/{room_id}/checklist.yaml (per-room) and content/gauntlet/cross_room_checks.yaml (cross-room). Each condition is evaluable from an ObserverSnapshot.
7 condition types: player_near, player_facing, entity_present, entity_absent, expected_monologue, expected_dialogue, expected_interaction_verb.
Schema: content/_schema/checklist.schema.json. The checklist format feeds into #503 (client auto-checklist progress tracking).
check-fact-ids operates in two modes:
- Advisory — when knowledge catalogs (
content/global/knowledge/*.yaml) have no fact definitions yet: lists referenced fact_ids and exits cleanly. - Enforcing — when catalogs are populated: fails on any
fact_idreference that doesn't match a canonical definition.
Asset Pipeline — Generator-Driven DB (#855, #856, #857)
server/data/systems.db is a read-only canonical snapshot produced by three
generators. It is committed to the repo so the client can ship it, but it is never
the source of truth. Direct edits are forbidden — they are silently overwritten by
the next regeneration.
Generators
| Generator | Source | Runs via |
|---|---|---|
generate_brands |
server/src/bin/generate_brands/main.rs |
tooling/generate-brands |
import_economics |
tooling/economy-db/import_economics.py |
python3 tooling/economy-db/import_economics.py |
generate_atlas |
tooling/planet-gen/generate_atlas.py |
python3 tooling/planet-gen/generate_atlas.py --seed 42 |
Run all three at once with:
make regen-db
Meta table stamp
After every successful non-dry-run, each generator writes a row to the meta table in
systems.db recording the SHA-1 of its source file(s) and the schema file.
make check-systems-db # Verify the stamp is fresh (exit 1 = stale)
Making a DB change
- Edit source files (TOML, JSON,
markers.json). make regen-dbgit add server/data/systems.db- Commit with
chore(db): regen systems.db — <reason>
For schema changes, also update server/data/systems-schema.sql and add migration DDL
to MIGRATION_SQL in import_economics.py.
See .claude/rules/asset-pipeline.md for the full rule set.
Pre-commit and Pre-push Hooks
Git hooks are stored in .config/hooks/ (version-controlled). Activate them with:
make setup # Includes hook installation
make install-hooks # Just hooks (also makes them executable)
Or manually:
git config core.hooksPath .config/hooks
Active checks:
| Hook | Check | Script | Behavior |
|---|---|---|---|
| pre-commit | fact_id validation | tooling/check-fact-ids |
Warns if catalogs are stubs; fails on unknown fact_ids when populated |
| pre-push | GDScript parse | internal | Fails on any SCRIPT ERROR |
| pre-push | Rust lint | internal | fmt + clippy |
| pre-push | Python lint | internal | ruff |
| pre-push | JSON syntax | internal | python3 -m json.tool |
| pre-push | systems.db stamp | tooling/check-systems-db-stamp |
Rejects stale DB when pushed (#857) |
The core.hooksPath setting uses a relative path (.config/hooks) that resolves per worktree, so it works correctly across all worktrees in the repository.
To bypass hooks in an emergency:
git commit --no-verify -m "fix: emergency hotfix"
Configuration Files
The .config/ directory holds shared configuration for linters, formatters, and CI. Examples of what goes here:
- Clippy configuration overrides
- gdlint/gdformat rules
- CI workflow definitions (before moving to
.github/workflows/) - Editor config templates
Project-specific config that lives in subdirectories (e.g., server/Cargo.toml, client/project.godot) stays in those directories. .config/ is for cross-cutting or shared configuration.
Cache Directory
.cache/ is gitignored and used for:
- Test result caches
- Linter caches
- Build artifact caches (if configured)
- Coverage reports
Agents and CI jobs can write freely to .cache/ without polluting the working tree. make clean clears it.
Testing Architecture (D-030)
Three-layer testing strategy:
- Unit tests — Inside
server/(Rust#[cfg(test)]) andclient/(gdUnit4). Test individual systems in isolation. - Integration tests — Inside
server/tests/(Rust) andtests/(cross-boundary). Test system interactions, IPC serialization round-trips. - Fixture-based tests — IPC serialization fixture files in
tests/for protocol regression testing. Known-good MessagePack payloads verified against both sides.
Key components:
- CauseChain (production ECS component) — Tracks causal attribution for testable observation sequences (D-030).
- Deterministic replay — Server simulation is deterministic given the same seed + input sequence. Replay logs enable regression testing (#201, critical).
Planning store (pql)
Tickets and decisions live in pql, not in the old SQLite wrapper scripts. Decisions
are markdown-sourced under governance/{decisions,questions,rejected}/; tickets live in
.pql/pql.db, rebuilt from the git-tracked .pql/changelog/. Never use the sqlite3
CLI — it crashes in Claude Code (std::bad_alloc).
pql ticket list --status in_progress # query tickets (T-NNN ids)
pql ticket status T-440 done # mutate — flushed to the changelog
pql decisions list --type confirmed # query decisions
pql decisions show D-010 --with-tickets
make decisions-sync # parse governance/*.md into pql.db
make decisions-validate # malformed-record gate
Markdown records + the changelog are the source of truth; pql.db is a rebuildable
index. The pre-commit hook runs pql decisions validate and exports ticket mutations to
the changelog; post-merge/checkout/rewrite hooks replay it. Full reference:
.claude/rules/ticket-cli.md.
Commit Conventions
See the /git-commit skill (.claude/skills/git-commit/) for full details. Summary:
- Conventional commits:
type(scope): summary - Types:
feat,fix,refactor,chore,docs,data,loc - Scopes match project subsystems:
client,server,engine,simulation,ui,audio,meta, etc. - Imperative mood, lowercase, no period, max 72 chars
- CHANGELOG.md updated after each commit group