From 346d87df7a220f2296bc04d926e81d2e8714755f Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 12 Jun 2026 16:22:55 +0200 Subject: [PATCH] chore(meta): docs/build sweep + tooling test gate (T-1069, T-1066) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - make test-tooling: planet-gen determinism guard + import_economics --dry-run, wired into pre-push on TOOLING_CHANGED; ruff widened to E4/E7/E9/F/W (90 safe auto-fixes applied; E402/E702/F841 ignored with documented counts) - one-generator reality fixed in DEVOPS.md, asset-pipeline rule, CLAUDE.md (import_economics sole generator since #951/D-223); dead check-protocol target deleted; DEVOPS hook/config sections rewritten from the actual hook sources; team-patterns gate description updated (client+tooling) - project.yaml: 0.2.0 → 0.4.0 per the 0.{phase}.{n} scheme, description refreshed from the v0.1 Sova narration to cascade reality - stale comment sweep: voxel.rs stub claims (all 8 families implemented), cascade.rs TODO recited to T-1044, main.rs D-192 handshake claim, relationships.rs/chunk_streaming.rs version targets → phase language - gitignore: client/settings.db* e2e-run artifacts Co-Authored-By: Claude Fable 5 --- .claude/rules/asset-pipeline.md | 29 +++--- .claude/rules/team-patterns.md | 10 ++- .gitignore | 5 ++ CLAUDE.md | 5 +- Makefile | 45 +++++++--- docs/DEVOPS.md | 88 +++++++++++++------ project.yaml | 28 ++++-- pyproject.toml | 18 ++-- server/src/atlas/cascade.rs | 2 +- server/src/atlas/voxel.rs | 34 +++---- server/src/main.rs | 3 +- server/src/npc/relationships.rs | 5 +- server/src/simulation/chunk_streaming.rs | 10 ++- tooling/assign-astro-ids.py | 2 +- tooling/blender_create_clothing_refs.py | 4 +- tooling/blender_process_bodies.py | 2 +- tooling/blender_process_hair.py | 2 +- tooling/blender_process_heads.py | 2 +- tooling/blender_surface_deform_batch.py | 12 +-- tooling/check_icosphere.py | 3 +- tooling/db/audio_batch.py | 6 +- tooling/db/audio_connector.py | 2 +- tooling/db/audio_post.py | 2 +- tooling/db/backfill_cultural_corridor.py | 16 ++-- tooling/db/image_connector.py | 2 +- tooling/db/populate_gttr_hook.py | 8 +- tooling/db/trellis_connector.py | 3 +- tooling/generate-star-map-svg.py | 13 ++- tooling/generate-star-map.py | 34 +++---- tooling/glb_strip_utility_nodes.py | 2 +- tooling/migrate-s-to-gj.py | 4 +- tooling/planet-gen/atlas_cohesion_audit.py | 2 +- tooling/planet-gen/batch.py | 6 +- .../planet-gen/import_province_boundaries.py | 6 +- tooling/planet-gen/planet_renderer.py | 4 +- tooling/planet-gen/planet_simulation.py | 5 +- .../planet-gen/populate_terrain_reference.py | 10 +-- tooling/planet-gen/render_heightmap.py | 4 +- tooling/planet-gen/sol_data/luna.py | 2 +- tooling/planet-gen/sol_data/mars.py | 2 +- tooling/planet-gen/sol_data/venus.py | 2 +- tooling/planet-gen/sol_import.py | 6 +- tooling/pql-migrate/retag_ticket_refs.py | 2 +- tooling/pql-migrate/seed_tickets.py | 2 +- tooling/setup_clothing_metadata.py | 2 +- tooling/tune-star-map-topology.py | 2 +- tooling/wiki/generate-stubs.py | 2 +- 47 files changed, 280 insertions(+), 180 deletions(-) diff --git a/.claude/rules/asset-pipeline.md b/.claude/rules/asset-pipeline.md index 50ad10ab5..5154484a4 100644 --- a/.claude/rules/asset-pipeline.md +++ b/.claude/rules/asset-pipeline.md @@ -11,7 +11,8 @@ client can ship it without a build step, but **it is never the source of truth** > **Edit sources, not the DB.** If you need to change economics data, modify the TOML/JSON source files. -If you need to change atlas markers, modify the `markers.json` files. +If you need to change the atlas city-name pool, modify the names-only +`markers.json` files (D-223 — they carry no geometry or population). Never run `UPDATE` or `INSERT` directly on `server/data/systems.db` outside of a migration — those changes will be silently overwritten by the next `make regen-db`. @@ -19,12 +20,12 @@ migration — those changes will be silently overwritten by the next `make regen ## What produces systems.db -Two generators write to `systems.db`: - -| Generator | Command | Source files (all contribute to the meta stamp SHA) | -|-----------|---------|--------------| -| `import_economics` | `python3 tooling/economy-db/import_economics.py` | `tooling/economy-db/import_economics.py` + the Rust brand binary sources it invokes: `server/src/bin/generate_brands/main.rs`, `server/src/bin/generate_brands/names.rs`, `tooling/generate-brands` + shared `tooling/schema_version.py` | -| `generate_atlas` | `python3 tooling/planet-gen/generate_atlas.py --seed 42` | `tooling/planet-gen/generate_atlas.py` + shared `tooling/schema_version.py` | +**One generator** writes to `systems.db`: `import_economics` +(`python3 tooling/economy-db/import_economics.py`, run via `make regen-db`). +The former atlas geometry generator (`generate_atlas`) was retired in #951 +(D-223); `import_economics` now also owns the atlas index — it loads the +names-only `markers.json` city pool into `atlas_city_names` and empties the +geometry tables (the server cascade fills them, Phase 4). `import_economics` shells out to the Rust `generate_brands` binary as its first step to refresh `wiki/economics/corporations/generated_brands.toml`, then reads @@ -33,17 +34,25 @@ of the Python importer, not an independent generator — changes to its source invalidate the `import_economics` meta stamp even though the Python file itself didn't change. -`make regen-db` runs both in the correct order (economics first, atlas second). +The full set of source files contributing to the meta stamp SHA (the Python +importer, the Rust brand binary sources, `tooling/schema_version.py`, and the +authored economics TOMLs) is registered in the `GENERATOR_SOURCES` dict at the +top of `tooling/check-systems-db-stamp` — that dict is the single source of +truth, mirrored by `IMPORT_ECONOMICS_SOURCES` in `import_economics.py`. + +The surviving planet-gen importers (`import_heightmaps`, +`import_province_boundaries`) are one-time build imports baked into the +committed DB — not part of `make regen-db`, and intentionally not stamped. --- ## The meta table stamp (T-855, T-856) -After every successful non-dry-run, each generator writes a row to the `meta` table: +After every successful non-dry-run, the generator writes a row to the `meta` table: ```sql CREATE TABLE meta ( - generator_name TEXT PRIMARY KEY, -- 'import_economics' | 'generate_atlas' + generator_name TEXT PRIMARY KEY, -- 'import_economics' (sole generator since #951/D-223) schema_version TEXT NOT NULL, -- monotonic semver string (e.g. "1.0.0") — see T-888 schema_sha TEXT, -- SHA-1 of server/data/systems-schema.sql (tamper detection) generator_sha TEXT NOT NULL, -- SHA-1 of the generator source file(s) diff --git a/.claude/rules/team-patterns.md b/.claude/rules/team-patterns.md index 0b1915638..bdec3dda4 100644 --- a/.claude/rules/team-patterns.md +++ b/.claude/rules/team-patterns.md @@ -34,10 +34,12 @@ When leading a team (workshop, batch, or any multi-agent session): ## Agent verification — don't duplicate the push gate -The pre-push hook (`.config/hooks/pre-push`) is the source of truth for Rust -verification and **runs on every push the lead makes** when `server/` changed: -`cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test` -(plus `cargo deny`, ruff, JSON validation, systems.db stamp). There is no CI (no +The pre-push hook (`.config/hooks/pre-push`) is the source of truth for +verification and **runs on every push the lead makes**: when `server/` changed — +`cargo fmt --check`, `cargo clippy --all-targets -- -D warnings`, and `cargo test`; +when `client/` changed — the full gdUnit4 suite via `tests/run-godot` (T-1065); +when `tooling/` changed — `make test-tooling` (plus `cargo deny`, ruff, JSON +validation, systems.db stamp). There is no CI (no `.gitea`/`.github`/`.forgejo` workflows) — the push gate is the only automatic verification, so it is deliberately comprehensive. diff --git a/.gitignore b/.gitignore index 7281c8055..287654c09 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,8 @@ wiki/economics/corporations/generated_corporations.toml # pql's durable, git-versioned planning state (LWW, idempotent replay). .pql/*.db .pql/*.db-* + +# Client runtime artifacts (settings store written by e2e test runs) +client/settings.db +client/settings.db-shm +client/settings.db-wal diff --git a/CLAUDE.md b/CLAUDE.md index bbba2418d..ed7c09f48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,8 +27,9 @@ See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. A ### Asset pipeline -`server/data/systems.db` is a read-only canonical snapshot produced by the generator -pipeline — never edit it directly. To regenerate: `make regen-db`. Full rules in +`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 diff --git a/Makefile b/Makefile index d92f00f2a..bc30cdfaf 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null) -.PHONY: help setup build check-protocol client server game stop test lint lint-python setup-venv ci ci-client ci-server clean \ +.PHONY: help setup build client server game stop test test-tooling lint lint-python setup-venv ci ci-client ci-server clean \ decisions-sync decisions-active decisions-validate \ validate-content check-fact-ids setup-hooks install-hooks \ audit deny atlas-verify economy-db regen-db check-systems-db \ @@ -30,6 +30,7 @@ help: @echo " make client Run the Godot client (test mode)" @echo " make server Run the Rust simulation server" @echo " make test Run all tests" + @echo " make test-tooling Tooling test gate: sim determinism + economics dry-run" @echo " make test-ipc-fixtures Layer 1: IPC serialization fixtures" @echo " make test-ipc-protocol Layer 2: mock IPC protocol tests" @echo " make test-ipc-integration Layer 3: real subprocess round-trip" @@ -39,7 +40,6 @@ help: @echo " make ci Run full CI pipeline locally" @echo " make ci-client Run client CI checks" @echo " make ci-server Run server CI checks" - @echo " make check-protocol Verify server/client protocol versions match" @echo " make clean Remove build artifacts and caches" @echo "" @echo " make decisions-sync Sync governance/*.md decision records into pql.db" @@ -120,16 +120,7 @@ setup-venv: # --- Build --- -check-protocol: - @SERVER_V=$$(grep 'pub const PROTOCOL_VERSION' server/src/bridge/types.rs | sed 's/.*= *//;s/[^0-9]//g'); \ - CLIENT_V=$$(grep 'const PROTOCOL_VERSION' client/scripts/protocol/protocol.gd | sed 's/.*= *//;s/[^0-9]//g'); \ - if [ "$$SERVER_V" != "$$CLIENT_V" ]; then \ - echo "ERROR: Protocol version mismatch — server=$$SERVER_V, client=$$CLIENT_V"; \ - echo " Fix: update client/scripts/protocol/protocol.gd to match server/src/bridge/types.rs"; \ - exit 1; \ - fi - -build: check-protocol build-server build-client +build: build-server build-client build-server: cd server && cargo build @@ -227,6 +218,36 @@ test-ipc-integration: test-ipc-benchmark: tests/run-ipc-benchmark +# Python with tooling deps (numpy/scipy) — .venv from `make setup-venv`, else system python3 +VENV_PY := $(shell test -x .venv/bin/python && echo .venv/bin/python || echo python3) + +# Tooling test gate (T-1066) — called by pre-push when tooling/ changed. +# 1. planet-gen determinism guard (#963): simulating the same body twice must +# be bit-identical (guards the expensive 271-body heightmap bake). +# Exit 2 = no testable body found — warn, don't block. +# 2. import_economics --dry-run against the committed DB: full parse + +# structural/coverage validation, no writes. Exit 2 = coverage-gate +# warning (D-175) — tolerated, matching regen-db's treatment. +test-tooling: + @echo " [test-tooling] planet-gen determinism guard (#963)..." + @rc=0; $(VENV_PY) tooling/planet-gen/test_sim_determinism.py || rc=$$?; \ + if [ $$rc -eq 2 ]; then \ + echo " WARNING: determinism guard found no testable body (exit 2) — not blocking"; \ + elif [ $$rc -ne 0 ]; then \ + echo " FAIL: planet_simulation determinism drift (exit $$rc)"; exit $$rc; \ + fi + @echo " [test-tooling] import_economics --dry-run (committed DB)..." + @mkdir -p .cache + @rc=0; python3 tooling/economy-db/import_economics.py --dry-run \ + > .cache/test-tooling-dryrun.log 2>&1 || rc=$$?; \ + if [ $$rc -eq 2 ]; then \ + echo " WARNING: coverage gate warning (exit 2) — not blocking (matches regen-db)"; \ + elif [ $$rc -ne 0 ]; then \ + echo " FAIL: import_economics --dry-run exited $$rc — log follows:"; \ + cat .cache/test-tooling-dryrun.log; exit $$rc; \ + fi + @echo " test-tooling: PASS" + # --- Clean --- clean-imports: diff --git a/docs/DEVOPS.md b/docs/DEVOPS.md index 7579f3639..2d6c8c9be 100644 --- a/docs/DEVOPS.md +++ b/docs/DEVOPS.md @@ -73,6 +73,7 @@ The server must be running before the client connects (subprocess launch will be 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: @@ -210,20 +211,21 @@ Schema: `content/_schema/checklist.schema.json`. The checklist format feeds into ## 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 +`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. -### Generators +### Generator -| 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` | +`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 all three at once with: +Run it with: ```bash make regen-db @@ -231,8 +233,9 @@ 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. +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`. ```bash make check-systems-db # Verify the stamp is fresh (exit 1 = stale) @@ -265,16 +268,43 @@ Or manually: git config core.hooksPath .config/hooks ``` -Active checks: +### Pre-commit checks (`.config/hooks/pre-commit`) -| 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) | +| 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/`, 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. @@ -282,18 +312,22 @@ To bypass hooks in an emergency: ```bash git commit --no-verify -m "fix: emergency hotfix" +git push --no-verify ``` ## Configuration Files -The `.config/` directory holds shared configuration for linters, formatters, and CI. Examples of what goes here: +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`). -- 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. +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 diff --git a/project.yaml b/project.yaml index 97fa4b7d2..d36c4dc02 100644 --- a/project.yaml +++ b/project.yaml @@ -1,19 +1,29 @@ name: The Settled Reach -version: 0.2.0 +# Version scheme: 0.{phase}.{n} — phase = active Development Cascade phase (D-166). +# Phase 4 (deterministic world generation) is active. +version: 0.4.0 repository: settled-reach description: > - Top-down immersive sim with occlusion-based detection mechanics and - combat elements. Single-character perspective where asymmetric information - is the core gameplay mechanic. A Rimworld-style storyteller drives - emergent narrative across a detective-smuggler dual-lens campaign. + Top-down life-sim set in an original science fiction universe. Asymmetric + information and occlusion-based perception from a single-character + perspective, with a Rimworld-style storyteller. Systems interactions like + The Sims, combat and visuals like single-character Rimworld, world + generation inheriting from Dwarf Fortress and NMS, economy inspired by + X4/EVE. Every system is interactable but ignorable — the world is alive + for any given run. setting: > - Original science fiction universe — the Settled Reach, a network of - star systems connected by Founder Gates. Neural lattice technology - enables soft immortality, forking, and re-embodiment. The v0.1 - vertical slice takes place in Sova Transit District, Van Maanen's Star. + The Settled Reach — a network of star systems connected by Founder Gates. + Neural lattice technology enables soft immortality, forking, and + re-embodiment. + +development: > + Built outside-in along the six-phase Development Cascade (D-166): wiki + content and star map, economics layer, planetary maps and atlas, + deterministic world generation (active), player control and in-world + rendering, room-level detail. architecture: client: Godot 4 (GDScript) diff --git a/pyproject.toml b/pyproject.toml index 605305b31..70ea82f9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,16 @@ line-length = 100 target-version = "py311" [tool.ruff.lint] -# E9xx: Runtime syntax/encoding errors -# F401: Unused imports -# F811: Redefinition of unused name -# F821: Undefined name (catches missing imports like bare `os`) -select = ["E9", "F401", "F811", "F821"] +# Widened from {E9, F401, F811, F821} to the full ruff-default tiers + W (T-1066). +# E4: import placement/style +# E7: statement-level pitfalls (== None, bare except, lambda assignment, ...) +# E9: runtime syntax/encoding errors +# F: all pyflakes (unused imports/names, undefined names, f-string misuse, ...) +# W: whitespace + invalid escape sequences (zero violations at adoption) +select = ["E4", "E7", "E9", "F", "W"] +# Rules excluded at adoption because the existing violation count was not +# trivially fixable (T-1066) — re-enable per-rule as the debt is paid down: +# E402 (43×): module-import-not-at-top — script-style sys.path.insert before import +# E702 (41×): semicolon-paired assignments, deliberate style in planet-gen noise math +# F841 (21×): unused locals, mostly in numeric/diagnostic code — needs manual review +ignore = ["E402", "E702", "F841"] diff --git a/server/src/atlas/cascade.rs b/server/src/atlas/cascade.rs index d54c37410..92c178c81 100644 --- a/server/src/atlas/cascade.rs +++ b/server/src/atlas/cascade.rs @@ -216,7 +216,7 @@ pub fn run_cascade_from_heightmap( // result nor the TerrainAnalysis is stored on Layer1Output, so we re-run both // here. Pure → determinism preserved, but the drainage re-run is NOT free at // the ~6 000-regions/body working scale (D-203). - // PERF/TODO(T-1028): cache TerrainAnalysis on Layer1Output to drop this + // PERF/TODO(T-1044): cache TerrainAnalysis on Layer1Output to drop this // redundant drainage pass, and validate the combined cost against the D-239 §10 // ~45 ms/body budget in the T-1031 verification harness. This is now a LIVE // production cost: T-1032 wired the real body_params read, so every analyzed diff --git a/server/src/atlas/voxel.rs b/server/src/atlas/voxel.rs index 9606f499d..b9898d09d 100644 --- a/server/src/atlas/voxel.rs +++ b/server/src/atlas/voxel.rs @@ -38,14 +38,14 @@ //! eviction is by generation-counter LRU (oldest entry evicted when capacity //! is reached). Never written to disk. //! -//! ## Family dispatch (D-239 §5, T-1028) +//! ## Family dispatch (D-239 §5, T-1028/T-1029) //! -//! The 8-family tree is dispatched through `MorphologyFamily`. Only -//! `AlluvialPlain` is implemented here (T-1028). The other 7 families are -//! T-1029: their stubs return the AlluvialPlain output as a documented -//! placeholder — **production will not crash on them**, but the output is not -//! the correct final geometry for that family. The stubs are clearly marked -//! `// T-1029 — NOT YET IMPLEMENTED` so T-1029 can find and replace them. +//! The 8-family tree is dispatched through `MorphologyFamily`. All 8 family +//! generators are implemented: `AlluvialPlain` (the D-239 §5 fallback) landed +//! in T-1028; the other 7 (LavaField, FjordWall, CliffCoast, BraidedDelta, +//! DuneStrand, IncisedGorge, MeanderReach) landed in T-1029. Flat/water zones +//! without a dedicated family (OpenOcean, Lake, TidalFlat, Wetland) map to +//! `AlluvialPlain` per D-239 §5. //! //! ## D-010 compliance //! @@ -249,33 +249,25 @@ pub type VoxelPos = (i32, i32); /// pre-computed at zone classification time (RegionProfile); the family is the /// structural decision that drives voxel geometry. /// -/// Only `AlluvialPlain` is implemented in T-1028. Other families return a -/// documented placeholder that falls back to AlluvialPlain geometry (not -/// `panic!` / `unimplemented!`). T-1029 replaces the stubs. +/// All 8 families have dedicated generators: `AlluvialPlain` (T-1028, also the +/// D-239 §5 fallback for flat/water zones) and the other 7 (T-1029). #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MorphologyFamily { /// Lava field / shield slope. Requires TectonicClass::Volcanic (D-239 §5). - /// T-1029 — NOT YET IMPLEMENTED: falls back to AlluvialPlain. LavaField, /// Fjord wall. Requires GlaciationGrade ≥ 2 (D-239 §5). - /// T-1029 — NOT YET IMPLEMENTED: falls back to AlluvialPlain. FjordWall, /// Cliff coast. High slope + coastal (D-239 §5). - /// T-1029 — NOT YET IMPLEMENTED: falls back to AlluvialPlain. CliffCoast, /// Braided delta. Very flat + low elevation + coastal (D-239 §5). - /// T-1029 — NOT YET IMPLEMENTED: falls back to AlluvialPlain. BraidedDelta, /// Dune strand. Low slope + coastal + arid (D-239 §5). - /// T-1029 — NOT YET IMPLEMENTED: falls back to AlluvialPlain. DuneStrand, /// Incised gorge / mountain pass. High slope + inland + high elev (D-239 §5). - /// T-1029 — NOT YET IMPLEMENTED: falls back to AlluvialPlain. IncisedGorge, /// Meander reach. Gentle slope + water presence (D-239 §5). - /// T-1029 — NOT YET IMPLEMENTED: falls back to AlluvialPlain. MeanderReach, - /// Alluvial plain — fallback (D-239 §5). **IMPLEMENTED** in T-1028. + /// Alluvial plain — the D-239 §5 fallback family (T-1028). AlluvialPlain, } @@ -301,8 +293,8 @@ fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily { MorphologyFamily::IncisedGorge } MorphologyZone::MeanderReach | MorphologyZone::RiverBank => MorphologyFamily::MeanderReach, - // AlluvialPlain fallback covers: AlluvialPlain, OpenOcean, Lake, TidalFlat, - // Wetland — any zone without a fully-implemented T-1029 generator. + // AlluvialPlain fallback covers the flat/water zones without a dedicated + // family: AlluvialPlain, OpenOcean, Lake, TidalFlat, Wetland (D-239 §5). MorphologyZone::AlluvialPlain | MorphologyZone::OpenOcean | MorphologyZone::Lake @@ -401,7 +393,7 @@ pub fn derive_voxel_column( // AlluvialPlain generator (T-1028, D-239 §5 fallback) // --------------------------------------------------------------------------- -/// AlluvialPlain voxel generator — the only fully implemented family in T-1028. +/// AlluvialPlain voxel generator — the D-239 §5 fallback family (T-1028). /// /// Flat floodplain with: /// - `Soil` terrain material (D-239 §8 Soil law: rolling/floodplain) diff --git a/server/src/main.rs b/server/src/main.rs index d7d7857ea..6e9fa5fed 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -124,7 +124,8 @@ fn main() { tracing::info!("Client connected, sending protocol handshake"); // Protocol handshake: first framed message on the wire (#555). - // Client reads this and validates protocol_version before sending any input. + // Carries no version field (D-192 dropped the PROTOCOL_VERSION lockstep); + // the client reads it and replies with its StartupMessage. use settled_reach_server::bridge::SimBridge; bridge.send_handshake().unwrap_or_else(|e| { tracing::error!("Failed to send handshake: {}", e); diff --git a/server/src/npc/relationships.rs b/server/src/npc/relationships.rs index 1e9f7711e..1091c4453 100644 --- a/server/src/npc/relationships.rs +++ b/server/src/npc/relationships.rs @@ -89,8 +89,9 @@ pub struct RelationshipEdge { /// BTreeMap<(subject, target), edge> for deterministic iteration (D-010). /// Directed graph: edge (A, B) represents how A feels about B. /// -/// TODO(v0.2): RelationshipGraph is per-world. Multiplayer needs per-observer -/// relationship views (D-010). +/// TODO(post-Phase-6): RelationshipGraph is per-world. Multiplayer — if it ever +/// lands, it is beyond the D-166 cascade — needs per-observer relationship +/// views (D-010). #[derive(Resource, Debug, Clone, Default, Serialize, Deserialize)] pub struct RelationshipGraph { edges: BTreeMap<(StableId, StableId), RelationshipEdge>, diff --git a/server/src/simulation/chunk_streaming.rs b/server/src/simulation/chunk_streaming.rs index 3010809b9..dfe7a3537 100644 --- a/server/src/simulation/chunk_streaming.rs +++ b/server/src/simulation/chunk_streaming.rs @@ -1,9 +1,10 @@ //! Chunk streaming system (#578, D-012). //! //! Loads chunks near the player and unloads distant chunks based on a -//! configurable radius. For v0.1 the radius covers the entire hand-authored +//! configurable radius. Until Phase 5 the radius covers the entire hand-authored //! district (256×256 visual tiles = 8×8 chunks of 32 tiles), so all chunks -//! remain loaded. The architecture supports future per-demand loading (v0.3+). +//! remain loaded. The architecture supports per-demand loading over the +//! generated world once player control lands (Phase 5). //! //! The system runs on a configurable tick cadence (default: every 10 ticks). //! It queries the player's TilePosition, computes which chunks should be @@ -17,8 +18,9 @@ use crate::simulation::time::SimulationTime; /// How many chunks around the player to keep loaded (Chebyshev distance). /// -/// Default: 8, which covers the full v0.1 district (256×256 = 8×8 chunks). -/// For v0.3+ borderless generation, set to 3-4 for memory-bounded streaming. +/// Default: 8, which covers the full hand-authored district (256×256 = 8×8 +/// chunks). For Phase 5 borderless streaming over the generated world, set to +/// 3-4 for memory-bounded loading. #[derive(Resource, Debug, Clone)] pub struct ChunkLoadRadius { pub radius: i32, diff --git a/tooling/assign-astro-ids.py b/tooling/assign-astro-ids.py index b41c3c843..a6bf484a1 100644 --- a/tooling/assign-astro-ids.py +++ b/tooling/assign-astro-ids.py @@ -256,7 +256,7 @@ def main(): for node in nodes: sp = simplify_spectral(node.get("spectral_class", "")) type_counts[sp] = type_counts.get(sp, 0) + 1 - print(f"\nSimplified spectral distribution:") + print("\nSimplified spectral distribution:") for t in ["G", "K", "M", "F", "unusual"]: print(f" {t:8s} {type_counts.get(t, 0):3d} ({type_counts.get(t, 0)/len(nodes)*100:.0f}%)") diff --git a/tooling/blender_create_clothing_refs.py b/tooling/blender_create_clothing_refs.py index f06a8bb9f..96ca225d9 100644 --- a/tooling/blender_create_clothing_refs.py +++ b/tooling/blender_create_clothing_refs.py @@ -266,7 +266,7 @@ if __name__ == "__main__": print(f"ERROR: Reference body directory not found: {avg_m_dir}") sys.exit(1) - print(f"\nClothing Reference Mesh Creator (v0.2 placeholder)") + print("\nClothing Reference Mesh Creator (v0.2 placeholder)") print(f" Bodies dir: {bodies_dir}") print(f" Output dir: {clothing_output_dir}") print(f" Reference: {REFERENCE_BODY}") @@ -280,7 +280,7 @@ if __name__ == "__main__": # Summary print(f"\n{'='*60}") - print(f"=== Clothing reference creation complete ===") + print("=== Clothing reference creation complete ===") ok_items = [k for k, v in results.items() if v] fail_items = [k for k, v in results.items() if not v] for item_id in CLOTHING_ITEMS: diff --git a/tooling/blender_process_bodies.py b/tooling/blender_process_bodies.py index b73402224..3cbfcdcc4 100644 --- a/tooling/blender_process_bodies.py +++ b/tooling/blender_process_bodies.py @@ -131,7 +131,7 @@ if __name__ == "__main__": # Summary print(f"\n{'='*60}") - print(f"=== Body type segmentation complete ===") + print("=== Body type segmentation complete ===") total_exported = 0 for body_type, exported, skipped, is_fork in results: flag = " [FORK]" if is_fork else "" diff --git a/tooling/blender_process_hair.py b/tooling/blender_process_hair.py index 5a1dcbc32..6ac379f7a 100644 --- a/tooling/blender_process_hair.py +++ b/tooling/blender_process_hair.py @@ -232,7 +232,7 @@ if __name__ == "__main__": results.append((tag, key, os.path.getsize(glb_path))) # bald.glb placeholder - print(f"\n[hair] bald (placeholder)") + print("\n[hair] bald (placeholder)") bald_path = os.path.join(hair_out, "bald.glb") make_bald_placeholder(bald_path) results.append(("hair", "bald", os.path.getsize(bald_path))) diff --git a/tooling/blender_process_heads.py b/tooling/blender_process_heads.py index 8df23acaf..c4e29f7a0 100644 --- a/tooling/blender_process_heads.py +++ b/tooling/blender_process_heads.py @@ -82,7 +82,7 @@ def process_head(blend_path: str, output_dir: str, head_id: str) -> None: bpy.ops.object.select_all(action='SELECT') # Export GLB — embedded textures, no animations - print(f" Exporting GLB...") + print(" Exporting GLB...") bpy.ops.export_scene.gltf( filepath=glb_path, use_selection=True, diff --git a/tooling/blender_surface_deform_batch.py b/tooling/blender_surface_deform_batch.py index e0a03f7c2..009a8a363 100644 --- a/tooling/blender_surface_deform_batch.py +++ b/tooling/blender_surface_deform_batch.py @@ -296,7 +296,7 @@ def process_body_type(body_type, reference_glb, bodies_dir, output_dir): # average_m is a direct copy -- no deform required if body_type == REFERENCE_BODY: - print(f" Reference body type -- copying reference directly") + print(" Reference body type -- copying reference directly") shutil.copy2(reference_glb, output_path) return {"body_type": body_type, "status": "ok", "method": "copy"} @@ -322,7 +322,7 @@ def process_body_type(body_type, reference_glb, bodies_dir, output_dir): print(f" Clothing mesh: {len(clothing_obj.data.vertices)} vertices") # --- Build body surface --- - print(f" Building body surface from segments...") + print(" Building body surface from segments...") body_surface = build_body_surface(bodies_dir, body_type) if body_surface is None: return {"body_type": body_type, "status": "error", @@ -386,7 +386,7 @@ if __name__ == "__main__": print(f"\nWARNING: Missing body type directories (will skip): " f"{', '.join(missing_types)}") - print(f"\nSurface Deform Batch Pipeline") + print("\nSurface Deform Batch Pipeline") print(f" Reference: {reference_glb}") print(f" Bodies: {bodies_dir}") print(f" Output: {output_dir}") @@ -405,7 +405,7 @@ if __name__ == "__main__": # Summary print(f"\n{'='*60}") - print(f"=== Surface Deform batch complete ===") + print("=== Surface Deform batch complete ===") ok = [r for r in results if r["status"] == "ok"] errors = [r for r in results if r["status"] == "error"] skipped = [r for r in results if r["status"] == "skipped"] @@ -434,8 +434,8 @@ if __name__ == "__main__": print(f" SKIPPED: {len(skipped)}") if sw_count > 0: print(f"\n WARNING: {sw_count} body type(s) used Shrinkwrap fallback.") - print(f" Visually verify these variants at gameplay zoom — Shrinkwrap") - print(f" may produce pinching at extremities on extreme body types.") + print(" Visually verify these variants at gameplay zoom — Shrinkwrap") + print(" may produce pinching at extremities on extreme body types.") # Write pipeline_log.json for provenance tracking log_path = os.path.join(output_dir, "pipeline_log.json") diff --git a/tooling/check_icosphere.py b/tooling/check_icosphere.py index aa7cb7050..3ca77861a 100644 --- a/tooling/check_icosphere.py +++ b/tooling/check_icosphere.py @@ -1,5 +1,6 @@ """List all objects in a Quaternius .blend file.""" -import sys, bpy +import sys +import bpy if __name__ == "__main__": argv = sys.argv diff --git a/tooling/db/audio_batch.py b/tooling/db/audio_batch.py index 8b6af3c11..93af17721 100644 --- a/tooling/db/audio_batch.py +++ b/tooling/db/audio_batch.py @@ -235,14 +235,14 @@ def main(): # Health check if any SAO assets sao_assets = [a for a in assets if a.get("method") == "sao"] if sao_assets and not dry_run: - print(f"Checking SAO API health...", file=sys.stderr) + print("Checking SAO API health...", file=sys.stderr) health_cmd = [sys.executable, os.path.join(script_dir, "audio_connector.py"), "health"] result = subprocess.run(health_cmd, capture_output=True, text=True) if result.returncode != 0: print(json.dumps({"ok": False, "error": "SAO API health check failed", "details": result.stdout.strip()})) sys.exit(1) - print(f" SAO API is up.", file=sys.stderr) + print(" SAO API is up.", file=sys.stderr) total = len(assets) results = [] @@ -264,7 +264,7 @@ def main(): if skip_existing: ogg_path = os.path.join(output_dir, filename) if os.path.exists(ogg_path): - print(f" Skipping — already exists", file=sys.stderr) + print(" Skipping — already exists", file=sys.stderr) results.append({"id": asset_id, "status": "skipped", "reason": "exists"}) skipped += 1 continue diff --git a/tooling/db/audio_connector.py b/tooling/db/audio_connector.py index 89b78c6af..4d215b488 100644 --- a/tooling/db/audio_connector.py +++ b/tooling/db/audio_connector.py @@ -135,7 +135,7 @@ def generate(prompt, duration=10.0, steps=100, cfg=7.0, output=None, timeout=600 method="POST" ) - print(f"Submitting generation request...", file=sys.stderr) + print("Submitting generation request...", file=sys.stderr) print(f" Prompt: {prompt}", file=sys.stderr) print(f" Duration: {duration}s, Steps: {steps}, CFG: {cfg}", file=sys.stderr) diff --git a/tooling/db/audio_post.py b/tooling/db/audio_post.py index dcee95c62..57546dcfc 100644 --- a/tooling/db/audio_post.py +++ b/tooling/db/audio_post.py @@ -112,7 +112,7 @@ def cmd_pipeline(args): # Step 3: convert run_ffmpeg( ["-i", normalized, "-c:a", "libvorbis", "-q:a", str(args.quality), output], - f"step 3/3: converting to OGG" + "step 3/3: converting to OGG" ) # Clean up intermediates diff --git a/tooling/db/backfill_cultural_corridor.py b/tooling/db/backfill_cultural_corridor.py index a1c730cf6..732c5ab84 100755 --- a/tooling/db/backfill_cultural_corridor.py +++ b/tooling/db/backfill_cultural_corridor.py @@ -72,12 +72,12 @@ def main(): "SELECT COUNT(*) FROM bodies WHERE cultural_corridor IS NULL" ).fetchone()[0] - print(f"\n cultural_corridor backfill") + print("\n cultural_corridor backfill") print(f" DB: {db_path}") if args.dry_run: - print(f" Mode: DRY RUN") + print(" Mode: DRY RUN") print() - print(f" Before:") + print(" Before:") print(f" star_systems.cultural_corridor NULL: {before_systems_null}") print(f" bodies.cultural_corridor NULL: {before_bodies_null}") @@ -118,14 +118,14 @@ def main(): if args.dry_run: conn.rollback() print() - print(f" Would update:") + print(" Would update:") print(f" star_systems: {sys_rows_updated}") print(f" bodies: {body_rows_updated}") - print(f"\n Dry run — no changes written.") + print("\n Dry run — no changes written.") else: conn.commit() print() - print(f" Updated:") + print(" Updated:") print(f" star_systems: {sys_rows_updated}") print(f" bodies: {body_rows_updated}") @@ -137,13 +137,13 @@ def main(): "SELECT COUNT(*) FROM bodies WHERE cultural_corridor IS NULL" ).fetchone()[0] print() - print(f" After:") + print(" After:") print(f" star_systems.cultural_corridor NULL: {after_systems_null}") print(f" bodies.cultural_corridor NULL: {after_bodies_null}") # Show the distribution so the outcome is visible. print() - print(f" star_systems.cultural_corridor distribution:") + print(" star_systems.cultural_corridor distribution:") for corridor, count in conn.execute( "SELECT cultural_corridor, COUNT(*) FROM star_systems " "GROUP BY cultural_corridor ORDER BY COUNT(*) DESC" diff --git a/tooling/db/image_connector.py b/tooling/db/image_connector.py index e7eb3e1c1..164bea49b 100755 --- a/tooling/db/image_connector.py +++ b/tooling/db/image_connector.py @@ -139,7 +139,7 @@ def generate(prompt, output=None, aspect_ratio="1:1", image_size=None, method="POST" ) - print(f"Generating image...", file=sys.stderr) + print("Generating image...", file=sys.stderr) print(f" Prompt: {prompt}", file=sys.stderr) if input_image: print(f" Input image: {input_image}", file=sys.stderr) diff --git a/tooling/db/populate_gttr_hook.py b/tooling/db/populate_gttr_hook.py index c5992de68..c353f3b81 100755 --- a/tooling/db/populate_gttr_hook.py +++ b/tooling/db/populate_gttr_hook.py @@ -144,11 +144,11 @@ def main(): except sqlite3.OperationalError: pass # column already exists - print(f"\n populate_gttr_hook.py") + print("\n populate_gttr_hook.py") print(f" DB: {db_path}") print(f" max words: {args.max_words}") if args.dry_run: - print(f" Mode: DRY RUN") + print(" Mode: DRY RUN") print() rows = conn.execute( @@ -202,12 +202,12 @@ def main(): conn.close() - print(f"\n Done:") + print("\n Done:") print(f" updated: {updated}") print(f" missing: {missing}") print(f" unmatched: {unmatched}") if args.dry_run: - print(f"\n Dry run — no DB writes.") + print("\n Dry run — no DB writes.") print() diff --git a/tooling/db/trellis_connector.py b/tooling/db/trellis_connector.py index 8dc21f093..d122f5514 100755 --- a/tooling/db/trellis_connector.py +++ b/tooling/db/trellis_connector.py @@ -222,7 +222,8 @@ def generate(image_path, output=None, simplify=0.95, texture_size=1024, # Generate a session hash — Gradio uses this to maintain gr.State between # separate API calls. Without it, image_to_3d's output state is lost before # extract_glb can read it. - import random, string + import random + import string session = ''.join(random.choices(string.ascii_lowercase + string.digits, k=12)) print(f" Session: {session}", file=sys.stderr) diff --git a/tooling/generate-star-map-svg.py b/tooling/generate-star-map-svg.py index e548079ed..2ca1f699e 100644 --- a/tooling/generate-star-map-svg.py +++ b/tooling/generate-star-map-svg.py @@ -5,7 +5,11 @@ Usage: python3 tooling/generate-star-map-svg.py [output.svg] Default output: docs/diagrams/design/star-map-concentric.svg """ -import json, sqlite3, math, sys, os +import json +import sqlite3 +import math +import sys +import os SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(SCRIPT_DIR) @@ -238,10 +242,10 @@ grad_r = max(width, height) start_pct = fade_start / grad_r * 100 end_pct = fade_end / grad_r * 100 svg.append(f'') -svg.append(f' ') +svg.append(' ') svg.append(f' ') svg.append(f' ') -svg.append(f' ') +svg.append(' ') svg.append('') svg.append('') @@ -338,7 +342,8 @@ print(f'{len(positions)} nodes, {len(starmap["edges"])} edges') print(f'Hops 0-{FOLD_HOP-1} individual, {FOLD_HOP}+ folded ({len(hop_groups.get(FOLD_HOP, []))} nodes)') # Also render PNG -import shutil, subprocess +import shutil +import subprocess png_path = output_path.replace('.svg', '.png') if shutil.which('magick'): cmd = ['magick', '-density', '150', '-background', '#0a0a1a', output_path, png_path] diff --git a/tooling/generate-star-map.py b/tooling/generate-star-map.py index af8378ddf..4db382631 100644 --- a/tooling/generate-star-map.py +++ b/tooling/generate-star-map.py @@ -1171,8 +1171,8 @@ def generate_sector_d2( lines = [] lines.append(f"# Star Map — {sector_label}") - lines.append(f"# Sector map. Cross-sector connections shown as stub nodes (dashed border).") - lines.append(f"# Node color = settlement wave. Shape = topology type.") + lines.append("# Sector map. Cross-sector connections shown as stub nodes (dashed border).") + lines.append("# Node color = settlement wave. Shape = topology type.") lines.append("") lines.append("vars: {") lines.append(f' bg: "{D2_BG}"') @@ -1182,7 +1182,7 @@ def generate_sector_d2( lines.append("") # Root style - lines.append(f"direction: right") + lines.append("direction: right") lines.append("") lines.append(f'style.fill: "{D2_BG}"') lines.append(f'style.stroke: "{D2_ACC}"') @@ -1192,7 +1192,7 @@ def generate_sector_d2( # Legend lines.append("legend: Legend {") lines.append(f' style.fill: "{D2_BG}"; style.stroke: "{D2_ACC}"; style.font-color: "{D2_TXT}"') - lines.append(f' style.font-size: 10') + lines.append(' style.font-size: 10') for wave, stroke in WAVE_STROKE.items(): fill = WAVE_FILL[wave] w_label = wave.replace("_", " ").title() @@ -1227,9 +1227,9 @@ def generate_sector_d2( lines.append(f' style.fill: "{fill}"') lines.append(f' style.stroke: "{stroke}"') lines.append(f' style.font-color: "{D2_TXT}"') - lines.append(f' style.font-size: 9') + lines.append(' style.font-size: 9') if nid == "S-001": - lines.append(f' style.stroke-width: 3') + lines.append(' style.stroke-width: 3') lines.append("}") lines.append("") @@ -1240,12 +1240,12 @@ def generate_sector_d2( stub_sector_label = SECTOR_LABELS[stub_node["geographic_sector"]] label = f"{stub_id}\\n[{stub_sector_label}]" lines.append(f'{d2id}: "{label}" {{') - lines.append(f' shape: rectangle') + lines.append(' shape: rectangle') lines.append(f' style.fill: "{STUB_FILL}"') lines.append(f' style.stroke: "{STUB_STROKE}"') - lines.append(f' style.stroke-dash: 5') + lines.append(' style.stroke-dash: 5') lines.append(f' style.font-color: "{STUB_STROKE}"') - lines.append(f' style.font-size: 9') + lines.append(' style.font-size: 9') lines.append("}") lines.append("") @@ -1281,8 +1281,8 @@ def generate_sector_d2( if is_cross: lines.append(f"{edge_line}: {{") lines.append(f' style.stroke: "{color}"') - lines.append(f' style.stroke-dash: 4') - lines.append(f' style.stroke-width: 1') + lines.append(' style.stroke-dash: 4') + lines.append(' style.stroke-width: 1') lines.append("}") else: lines.append(f"{edge_line}: {{") @@ -1331,7 +1331,7 @@ def generate_overview_d2( lines.append(f' acc: "{D2_ACC}"') lines.append("}") lines.append("") - lines.append(f'direction: right') + lines.append('direction: right') lines.append(f'style.fill: "{D2_BG}"') lines.append(f'style.stroke: "{D2_ACC}"') lines.append(f'style.font-color: "{D2_TXT}"') @@ -1354,23 +1354,23 @@ def generate_overview_d2( fill, stroke = sector_colors[sector] d2id = sector.replace("_", "") lines.append(f'{d2id}: "{label}\\n{count} systems · {hubs} hubs" {{') - lines.append(f' shape: rectangle') + lines.append(' shape: rectangle') lines.append(f' style.fill: "{fill}"') lines.append(f' style.stroke: "{stroke}"') lines.append(f' style.font-color: "{D2_TXT}"') - lines.append(f' style.border-radius: 8') + lines.append(' style.border-radius: 8') lines.append("}") lines.append("") # Special Gateway callout lines.append('gateway_note: "GATEWAY (S-001)\\nDiplomatic Periphery · Core\\nSol aperture: dormant" {') - lines.append(f' shape: hexagon') + lines.append(' shape: hexagon') lines.append(f' style.fill: "{GATEWAY_FILL}"') lines.append(f' style.stroke: "{GATEWAY_STROKE}"') lines.append(f' style.font-color: "{D2_TXT}"') - lines.append(f' style.stroke-width: 3') + lines.append(' style.stroke-width: 3') lines.append("}") - lines.append(f'gateway_note -> core: "located in" {{') + lines.append('gateway_note -> core: "located in" {') lines.append(f' style.stroke: "{GATEWAY_STROKE}"; style.stroke-dash: 3') lines.append("}") lines.append("") diff --git a/tooling/glb_strip_utility_nodes.py b/tooling/glb_strip_utility_nodes.py index fc2a689bb..f036708be 100644 --- a/tooling/glb_strip_utility_nodes.py +++ b/tooling/glb_strip_utility_nodes.py @@ -154,7 +154,7 @@ def process_file(input_path: str, output_path: str): count, names = strip_utility_nodes(gltf) if count == 0: - print(f" No utility nodes found — file unchanged") + print(" No utility nodes found — file unchanged") if output_path != input_path: import shutil shutil.copy2(input_path, output_path) diff --git a/tooling/migrate-s-to-gj.py b/tooling/migrate-s-to-gj.py index 7d9bfb8c4..123075b6e 100644 --- a/tooling/migrate-s-to-gj.py +++ b/tooling/migrate-s-to-gj.py @@ -97,7 +97,7 @@ def migrate_database(mapping): print("Database not found, skipping.") return - print(f"Migrating systems.db...") + print("Migrating systems.db...") conn = sqlite3.connect(SYSTEMS_DB) cur = conn.cursor() @@ -155,7 +155,7 @@ def migrate_database(mapping): def migrate_wiki_pages(mapping): """Replace S-numbers in wiki page headers and topology sections.""" - print(f"Migrating wiki pages...") + print("Migrating wiki pages...") count = 0 # Build reverse: need to match S-numbers in text diff --git a/tooling/planet-gen/atlas_cohesion_audit.py b/tooling/planet-gen/atlas_cohesion_audit.py index bfbc64668..5d270fe7c 100644 --- a/tooling/planet-gen/atlas_cohesion_audit.py +++ b/tooling/planet-gen/atlas_cohesion_audit.py @@ -364,7 +364,7 @@ def main() -> None: if row: system_id = row["system_id"] - print(f"\nAtlas Cohesion Audit") + print("\nAtlas Cohesion Audit") print(f" DB: {db}") if system_id: print(f" System: {system_id}") diff --git a/tooling/planet-gen/batch.py b/tooling/planet-gen/batch.py index cb7d60db6..6d344efec 100644 --- a/tooling/planet-gen/batch.py +++ b/tooling/planet-gen/batch.py @@ -334,11 +334,11 @@ def main(): total_invalid = 0 error_rate_threshold = 0.50 - print(f"\n Planet Generator — Batch Mode") + print("\n Planet Generator — Batch Mode") print(f" Systems: {len(system_dirs)}") print(f" Heightmap: {hmap_w}×{hmap_h} Globe: {args.globe_size}×{args.globe_size}") if args.dry_run: - print(f" Mode: DRY RUN (validation only)") + print(" Mode: DRY RUN (validation only)") print() for system_dir in system_dirs: @@ -545,7 +545,7 @@ def _verify_determinism(wiki_systems: Path, n_samples: int, print(f" passed: {passed} failed: {failed}") if failed > 0: - print(f" WARNING: non-deterministic output detected!") + print(" WARNING: non-deterministic output detected!") if __name__ == "__main__": diff --git a/tooling/planet-gen/import_province_boundaries.py b/tooling/planet-gen/import_province_boundaries.py index 1dd1dfe39..47cb39c6f 100644 --- a/tooling/planet-gen/import_province_boundaries.py +++ b/tooling/planet-gen/import_province_boundaries.py @@ -479,12 +479,12 @@ def main() -> None: print(f"error: {db_path} not found", file=sys.stderr) sys.exit(1) - print(f"\n Province Boundary Import (#907)") + print("\n Province Boundary Import (#907)") print(f" DB: {db_path}") if args.dry_run: - print(f" Mode: DRY RUN (no DB writes)") + print(" Mode: DRY RUN (no DB writes)") if args.force: - print(f" Force: enabled (will overwrite existing rows)") + print(" Force: enabled (will overwrite existing rows)") print() conn = sqlite3.connect(str(db_path)) diff --git a/tooling/planet-gen/planet_renderer.py b/tooling/planet-gen/planet_renderer.py index 95f46548b..063fb0626 100644 --- a/tooling/planet-gen/planet_renderer.py +++ b/tooling/planet-gen/planet_renderer.py @@ -861,7 +861,9 @@ def render_globe( # --------------------------------------------------------------------------- if __name__ == "__main__": - import sys, os, time + import sys + import os + import time TEST_BODIES = [ { diff --git a/tooling/planet-gen/planet_simulation.py b/tooling/planet-gen/planet_simulation.py index 85dba1e27..15700a73d 100644 --- a/tooling/planet-gen/planet_simulation.py +++ b/tooling/planet-gen/planet_simulation.py @@ -858,7 +858,10 @@ def simulate(body_def: dict) -> dict: # --------------------------------------------------------------------------- if __name__ == "__main__": - import sys, json, time, os + import sys + import json + import time + import os from PIL import Image if len(sys.argv) < 2: diff --git a/tooling/planet-gen/populate_terrain_reference.py b/tooling/planet-gen/populate_terrain_reference.py index efd64526d..d4bb47833 100644 --- a/tooling/planet-gen/populate_terrain_reference.py +++ b/tooling/planet-gen/populate_terrain_reference.py @@ -81,10 +81,10 @@ def main(): ORDER BY b.system_id, b.body_id """).fetchall() - print(f"\n terrain_reference population pass") + print("\n terrain_reference population pass") print(f" DB: {db_path}") if args.dry_run: - print(f" Mode: DRY RUN") + print(" Mode: DRY RUN") print(f"\n {len(rows)} bodies with NULL terrain_reference\n") found = [] @@ -119,12 +119,12 @@ def main(): conn.commit() print(f"\n Committed {len(found)} terrain_reference updates.") else: - print(f"\n Dry run — no changes written.") + print("\n Dry run — no changes written.") conn.close() # Summary - print(f"\n Summary:") + print("\n Summary:") print(f" Updated: {len(found)}") print(f" Missing: {len(missing)}") print(f" Total: {len(rows)}\n") @@ -132,7 +132,7 @@ def main(): if missing: print(f" Action required: generate heightmaps for {len(missing)} bodies " f"before running the atlas importers (#901).") - print(f" Use: make generate-terrain (or run generate.py per body)\n") + print(" Use: make generate-terrain (or run generate.py per body)\n") if __name__ == "__main__": diff --git a/tooling/planet-gen/render_heightmap.py b/tooling/planet-gen/render_heightmap.py index ca2eeb728..72d551e3d 100644 --- a/tooling/planet-gen/render_heightmap.py +++ b/tooling/planet-gen/render_heightmap.py @@ -418,7 +418,9 @@ def render_heightmap(body_def: dict, # --------------------------------------------------------------------------- if __name__ == "__main__": - import sys, json, time + import sys + import json + import time if len(sys.argv) < 2: print("Usage: python3 render_heightmap.py body_def.json [--large]") diff --git a/tooling/planet-gen/sol_data/luna.py b/tooling/planet-gen/sol_data/luna.py index 824524edd..17d573370 100644 --- a/tooling/planet-gen/sol_data/luna.py +++ b/tooling/planet-gen/sol_data/luna.py @@ -95,7 +95,7 @@ def build_terrain(body_def: dict) -> dict: sea_level = 0.0 surface_water = np.zeros((GRID_H, GRID_W), dtype=bool) - print(f" elevation normalised") + print(" elevation normalised") # ── 2. Temperature ────────────────────────────────────────────────── temperature_K = temperature_grid_analytical( diff --git a/tooling/planet-gen/sol_data/mars.py b/tooling/planet-gen/sol_data/mars.py index 0882d071e..2b5b7aac5 100644 --- a/tooling/planet-gen/sol_data/mars.py +++ b/tooling/planet-gen/sol_data/mars.py @@ -92,7 +92,7 @@ def build_terrain(body_def: dict) -> dict: # Normalise to [0, 1] elevation = normalize_01(elevation_m, MARS_MIN_ELEV_M, MARS_MAX_ELEV_M) - print(f" elevation normalised") + print(" elevation normalised") # ── 2. Temperature ────────────────────────────────────────────────── # Analytical: equatorial ~210K, polar ~150K, elevation lapse diff --git a/tooling/planet-gen/sol_data/venus.py b/tooling/planet-gen/sol_data/venus.py index cb03ee662..772776fe5 100644 --- a/tooling/planet-gen/sol_data/venus.py +++ b/tooling/planet-gen/sol_data/venus.py @@ -70,7 +70,7 @@ def _load_magellan() -> np.ndarray: print(f" PDS also failed ({e3})") # All sources failed — fall through to procedural generation - print(f" WARNING: all Magellan sources failed, using procedural") + print(" WARNING: all Magellan sources failed, using procedural") return None diff --git a/tooling/planet-gen/sol_import.py b/tooling/planet-gen/sol_import.py index 38deea31a..bb5e1878d 100644 --- a/tooling/planet-gen/sol_import.py +++ b/tooling/planet-gen/sol_import.py @@ -190,7 +190,7 @@ def _generate_body(body_def: dict, hmap_w: int, hmap_h: int, if is_gas: # Gas giants: no terrain, renderer handles bands procedurally terrain = {} - print(f" terrain: gas giant (procedural bands)") + print(" terrain: gas giant (procedural bands)") elif body_id in REAL_DATA_BODIES: # Real-world data import module_name = REAL_DATA_BODIES[body_id] @@ -198,11 +198,11 @@ def _generate_body(body_def: dict, hmap_w: int, hmap_h: int, importer = _load_importer(module_name) terrain = importer.build_terrain(body_def) if download_only: - print(f" download complete, skipping render") + print(" download complete, skipping render") return elif body_id in PROCEDURAL_BODIES: # Fall through to standard procedural simulation - print(f" procedural simulation (irregular body)...") + print(" procedural simulation (irregular body)...") terrain = simulate(body_def) else: print(f" WARNING: no importer for {body_id}, using procedural") diff --git a/tooling/pql-migrate/retag_ticket_refs.py b/tooling/pql-migrate/retag_ticket_refs.py index 5bc18cb28..4718799dd 100644 --- a/tooling/pql-migrate/retag_ticket_refs.py +++ b/tooling/pql-migrate/retag_ticket_refs.py @@ -78,7 +78,7 @@ def main(): all_skips += [(f.split('/main/',1)[-1], s) for s in skips] if APPLY and rewrites: open(f, "w", encoding="utf-8").write(new) - print(f"\n--- SKIPPED (not rewritten), grouped by reason ---") + print("\n--- SKIPPED (not rewritten), grouped by reason ---") by_reason = {} for rel, (tok, reason) in all_skips: by_reason.setdefault(reason, []).append(f"{tok} ({os.path.basename(rel)})") diff --git a/tooling/pql-migrate/seed_tickets.py b/tooling/pql-migrate/seed_tickets.py index bc2889759..b520d2584 100644 --- a/tooling/pql-migrate/seed_tickets.py +++ b/tooling/pql-migrate/seed_tickets.py @@ -79,7 +79,7 @@ def main(): print(f" ticket_deps: {len(deps)}") print(f" ticket_labels: {len(labels)} (existing) + {len(ms_labels)} (milestone-derived)") print(f" ticket_history: {len(history)}") - print(f" milestone label mapping: " + ", ".join(f"{mid}->{ms_label(mid)}" for mid in milestones)) + print(" milestone label mapping: " + ", ".join(f"{mid}->{ms_label(mid)}" for mid in milestones)) print(f" multi/space decision_refs kept verbatim: {len(multi)}") print(f" comma-team tickets kept verbatim: {comma_team}") diff --git a/tooling/setup_clothing_metadata.py b/tooling/setup_clothing_metadata.py index 630ec5dce..b05612215 100644 --- a/tooling/setup_clothing_metadata.py +++ b/tooling/setup_clothing_metadata.py @@ -174,7 +174,7 @@ if __name__ == "__main__": clothing_dir = sys.argv[1] - print(f"\nClothing Metadata Setup") + print("\nClothing Metadata Setup") print(f" Output dir: {clothing_dir}") print(f" Items: {len(CLOTHING_ITEMS)}") diff --git a/tooling/tune-star-map-topology.py b/tooling/tune-star-map-topology.py index 61547df75..513c37aee 100644 --- a/tooling/tune-star-map-topology.py +++ b/tooling/tune-star-map-topology.py @@ -841,7 +841,7 @@ def main() -> None: assert gw_node["gate_topology"] == "hub", "Gateway topology changed!" assert gw_node["aperture_count"] == 5, f"Gateway aperture_count={gw_node['aperture_count']} (should be 5)" assert len(adj[GATEWAY_ID]) == 4, f"Gateway degree={len(adj[GATEWAY_ID])} (should be 4)" - print(f"\n Gateway constraint check: OK (degree=4, aperture=5, topology=hub)") + print("\n Gateway constraint check: OK (degree=4, aperture=5, topology=hub)") # ── Verify no aperture violations ──────────────────────────────────────── violations = [ diff --git a/tooling/wiki/generate-stubs.py b/tooling/wiki/generate-stubs.py index adee9badc..77da562c6 100644 --- a/tooling/wiki/generate-stubs.py +++ b/tooling/wiki/generate-stubs.py @@ -229,7 +229,7 @@ def main(): print(f"Output directory: {WIKI_DIR}") # Print a few examples - print(f"\nExample directories:") + print("\nExample directories:") dirs = sorted(WIKI_DIR.iterdir())[:5] for d in dirs: if d.is_dir():