chore(meta): docs/build sweep + tooling test gate (T-1069, T-1066)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
+61
-27
@@ -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/<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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+19
-9
@@ -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)
|
||||
|
||||
+13
-5
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
-21
@@ -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)
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}%)")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'<radialGradient id="fadeEdge" cx="{cx}" cy="{cy}" r="{grad_r}" gradientUnits="userSpaceOnUse">')
|
||||
svg.append(f' <stop offset="0%" stop-color="#0a0a1a" stop-opacity="0"/>')
|
||||
svg.append(' <stop offset="0%" stop-color="#0a0a1a" stop-opacity="0"/>')
|
||||
svg.append(f' <stop offset="{start_pct:.1f}%" stop-color="#0a0a1a" stop-opacity="0"/>')
|
||||
svg.append(f' <stop offset="{end_pct:.1f}%" stop-color="#0a0a1a" stop-opacity="1"/>')
|
||||
svg.append(f' <stop offset="100%" stop-color="#0a0a1a" stop-opacity="1"/>')
|
||||
svg.append(' <stop offset="100%" stop-color="#0a0a1a" stop-opacity="1"/>')
|
||||
svg.append('</radialGradient>')
|
||||
svg.append('</defs>')
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -861,7 +861,9 @@ def render_globe(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys, os, time
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
|
||||
TEST_BODIES = [
|
||||
{
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)})")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user