Three live-found rendering bugs, all invisible to green unit suites: - _draw_tile_mosaic placed tiles from absolute district (0,0); canvas- local (0,0) is held_center - held_n/2 everywhere else, and tile-mode held_n is the whole-body extent — the entire mosaic drew tens of thousands of px off-canvas. New pure district_to_canvas_local() + viewer accessors route every tile through the shared frame. - _maybe_reselect_rung updated held_n across crossings without recomputing _view_offset — the single-window composite landed off- canvas the moment any crossing happened (why District/Quarter were black too). New pure recompute_offset_for_held_n_change(). - _build_tile_texture created an unstored ImageTexture per _draw, racing the RenderingServer's deferred upload — CPU pixels correct, screen white. Per-tile-index texture cache, same reference-identity discipline as the single-window _cached_texture. Structural close of the twice-bitten 'nothing asserts pixels' gap: test_atlas_window_overlay_draw_smoke.gd renders overlay output into a SubViewport and asserts visible pixels for both modes — runs under a real driver (invocation documented in DEVOPS.md, visual_capture precedent; migration to the T-1157 harness noted on that ticket), skips loud-but-green under the gate's headless run (verified green-with-skips AND genuinely red with detection forced off). +7 geometry/crossing tests, all revert-verified. Targeted suites 270 green; gdlint clean.
18 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)
make test-tooling # Tooling gate: planet-gen determinism guard + import_economics --dry-run (T-1066)
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 a single
generator. 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.
Generator
import_economics (tooling/economy-db/import_economics.py) is the sole
generator. As its first step it shells out to the Rust generate_brands binary
(via tooling/generate-brands) to refresh generated_brands.toml, then imports
economics data and the atlas index (names-only city pool; geometry tables stay
empty for the Phase 4 server cascade). The former atlas geometry generator
(generate_atlas) was retired in #951 (D-223).
Run it with:
make regen-db
Meta table stamp
After every successful non-dry-run, the generator writes a row to the meta table in
systems.db recording the SHA-1 of its source files and the schema file. The source
registry is the GENERATOR_SOURCES dict in tooling/check-systems-db-stamp.
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
Pre-commit checks (.config/hooks/pre-commit)
| Check | Script | Behavior |
|---|---|---|
| fact_id validation | tooling/check-fact-ids |
Warns if catalogs are stubs; fails on unknown fact_ids when populated |
| Decision records | pql decisions validate |
Blocks on malformed decision records (warns if pql not on PATH) |
| Planning changelog | pql plan export --stage |
Flushes ticket mutations to .pql/changelog/ and stages them into the commit (warn-only on failure) |
| cargo audit | cargo audit |
Only when Cargo.toml/Cargo.lock is staged; blocks on advisories (warns if cargo-audit not installed) |
Pre-push checks (.config/hooks/pre-push)
There is no CI — the pre-push gate is the only automatic verification, so it is
deliberately comprehensive. Checks are scoped to what actually changed vs the remote
(origin/<branch>, falling back to origin/main for first pushes): client/ changes
gate the GDScript checks, server/ the Rust checks, tooling/ + pyproject.toml the
Python checks.
| Check | Runs when | Behavior |
|---|---|---|
| GDScript parse (headless Godot) | client/ changed |
Blocks on any SCRIPT ERROR (skipped if no client/.godot/ import) |
| gdlint | client/ changed |
Advisory until the codebase is clean |
| gdformat --check | client/ changed |
Advisory until the codebase is clean |
cargo fmt --check |
server/ changed |
Blocks |
cargo clippy --all-targets -- -D warnings |
server/ changed |
Blocks (skipped if no server/target/ — cold worktree) |
cargo test |
server/ changed |
Blocks — the only automatic correctness gate (skipped if no server/target/) |
cargo deny check |
server/ changed |
Blocks (only if cargo-deny installed and server/deny.toml exists) |
ruff check tooling/ |
tooling/ changed |
Blocks |
make test-tooling |
tooling/ changed |
Blocks — sim determinism guard + economics dry-run (T-1066) |
JSON syntax (python3 -m json.tool) |
any changed *.json |
Blocks on syntax errors |
| systems.db stamp | server/data/systems.db in push |
Blocks on stale stamp (#857); missing meta table warns only |
| Clerk review (D-221) | disabled (#965) | Force-run with SR_RUN_CLERK=1 |
Post-merge / post-checkout / post-rewrite
These hooks replay the pql planning changelog (pql plan import / rebuild) and
re-sync decisions from governance/*.md so the planning DB stays in step after
merges, checkouts, and history rewrites.
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"
git push --no-verify
Configuration Files
The .config/ directory currently holds exactly one thing: the version-controlled
git hooks in .config/hooks/ (pre-commit, pre-push, post-merge, post-checkout,
post-rewrite), activated via git config core.hooksPath .config/hooks
(make install-hooks).
Linter and formatter configuration lives with the code it governs, not in .config/:
ruff is configured in the root pyproject.toml ([tool.ruff]), Rust uses cargo
defaults plus server/deny.toml, and the client uses gdlint/gdformat defaults.
Project-specific config (e.g. server/Cargo.toml, client/project.godot) stays in
those directories. .config/ is reserved for future cross-cutting configuration
that has no better home.
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).
Real-rendering test exception: test_atlas_window_overlay_draw_smoke.gd
tests/run-godot hardcodes --headless, whose dummy driver produces no usable GPU
texture output (SubViewport.get_texture().get_image() returns unusable data). One
file needs real pixels — client/tests/test_atlas_window_overlay_draw_smoke.gd (T-1153
live round 4) renders AtlasWindowOverlay into a SubViewport and asserts real terrain
pixels were composited, closing a "did anything draw at all" gap that bit twice
(a tile-mosaic coordinate bug and an unstored-texture GPU-lifetime bug, both invisible
to cache-state-only assertions). It self-detects the dummy driver
(DisplayServer.get_name() == "headless") and skips under the standard suite —
tests/run-godot --filter test_atlas_window_overlay_draw_smoke reports green-with-skips,
never a false failure that would bounce the push gate. To exercise its real assertions:
godot4 --display-driver x11 --rendering-driver opengl3 \
-s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c \
-a res://tests/test_atlas_window_overlay_draw_smoke.gd
Same underlying constraint as tests/visual_capture.gd (tests/run-visual), which is
the project's other real-driver exception — currently broken on this branch by the
retired AtlasViewer API (T-1157, capture-harness redesign). This file's scenarios are
slated to migrate into that redesigned harness once T-1157 lands, folding it into
tests/visual.json for consistency; until then it stays a standalone gdUnit file with
its own skip guard.
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