- 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>
8.9 KiB
Asset Pipeline — Source-Canonical Rule
server/data/systems.db is a read-only, deterministic snapshot produced by the
generator pipeline. It is checked in to the repo as a build artefact so the Godot
client can ship it without a build step, but it is never the source of truth.
The Golden Rule
Edit sources, not the DB.
If you need to change economics data, modify the TOML/JSON source 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.
What produces systems.db
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
the TOML and imports brand data into the DB. The Rust binary is a subroutine
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.
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, the generator writes a row to the meta table:
CREATE TABLE meta (
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)
generated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
schema_version is a monotonic semver string (e.g. "1.0.0"), not a hash.
It is defined as the SCHEMA_VERSION constant in tooling/schema_version.py
and must be bumped manually whenever the schema changes in a backwards-incompatible way.
Unlike a SHA-1 hash, semver strings are orderable — this enables savegame migration
lineage in Phase 5+: a save file can record which schema version it derives from and
determine exactly which migrations to apply (T-888). The old SHA-1 is preserved in
schema_sha for tamper detection alongside the semver.
The generator_sha is the SHA-1 of the concatenated bytes of the generator's
source files (sorted by path, so order is deterministic). If any source file
changes and make regen-db is not re-run, the stamped SHA will differ from the
recomputed current SHA — this is what the pre-push hook detects.
What's deterministic: the stored SHA (same sources → same recorded SHA).
What's NOT deterministic: the DB binary itself. meta.generated_at uses
datetime('now'), SQLite rowid/autoincrement values drift across runs, and
transaction ordering can reshape freelist pages — two consecutive make regen-db
calls produce byte-different SQLite files even with identical inputs. This is
fine: the freshness guarantee comes from the stamp, not from bytewise DB equality.
How to make a DB change
Normal data changes (economics, atlas markers)
- Edit the source files (TOML, JSON, markers.json).
- Run
make regen-db. - Run
make check-systems-dbto confirm the stamp is fresh. - Stage and commit:
git add server/data/systems.db git commit -m "chore(db): regen systems.db — <what changed>"
Schema changes (new tables or columns)
- Add the DDL to
server/data/systems-schema.sql. - Add migration SQL to
MIGRATION_SQLinimport_economics.pyif the change affects existing DBs (idempotentCREATE TABLE IF NOT EXISTSorALTER TABLE). - Run
make regen-db. - Stage
server/data/systems-schema.sqlandserver/data/systems.dbtogether.
Pre-push hook (T-857)
.config/hooks/pre-push (installed via make install-hooks) checks that whenever
server/data/systems.db is in the push, its meta stamp matches the current generator
source SHAs. If not, the push is rejected with:
systems.db is stale — run `make regen-db` before pushing.
Stale generators: ['import_economics']
Fix: run make regen-db, stage server/data/systems.db, amend or add a commit.
Or use /pr-push — it detects stale generator sources and reruns make regen-db
automatically before pushing.
The check script is tooling/check-systems-db-stamp. Run it interactively with
make check-systems-db or python3 tooling/check-systems-db-stamp --verbose. The
GENERATOR_SOURCES dict at the top of that script is the single registry — when
you add a new generator or source file, update it there and mirror the change in
the /pr-push skill's source-file watch list.
/pr-push integration (T-858)
The /pr-push skill checks whether any generator source files are modified on the
branch. If they are, it automatically runs make regen-db and stages the updated
server/data/systems.db before pushing — preventing pre-push hook rejections on
branches that modify generators without regenerating.
Why direct DB edits are forbidden
Two branches that both commit server/data/systems.db changes produce a binary
merge conflict. Git cannot diff or merge binary SQLite files. Sprint 36 hit this
exact class of problem. The meta stamp + pre-push hook is the systematic fix:
- The stamp is deterministic (same generator source → same recorded SHA)
- Only one branch modifies generator sources at a time (per team scope rules)
- The pre-push hook is a hard blocker before the binary conflict can land
The migration escape hatch
The rule above says "never run UPDATE or INSERT directly on systems.db outside of a migration." Here's what a legitimate migration looks like, and what isn't one:
Sanctioned path: the MIGRATION_SQL block in import_economics.py. That
string is executed at the top of every import run (inside the same transaction
that clears + reimports data) and contains idempotent CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS statements, plus ALTER TABLE additions
handled via the COLUMN_MIGRATIONS list. When you need a new table, column,
or index on systems.db, add it there. It'll run on the next make regen-db
and the meta stamp will flip because import_economics.py changed.
Also legitimate: edits to server/data/systems-schema.sql (the canonical
DDL used by fresh builds) paired with matching entries in MIGRATION_SQL for
existing DBs. The stamp's schema_version field records the schema file's
SHA at generation time — change the schema, commit both files together, and
the stamp picks it up automatically.
NOT legitimate and forbidden:
- Running any raw SQL (e.g. the
sqlite3CLI) againstsystems.dbby hand. Any changes you make are silently reverted by the nextregen-dbrun — your edits die, not the pipeline's. - One-off patch scripts that open
systems.dband modify rows. - Editing the DB file with a SQLite GUI.
- Committing
systems.dbalone, without the corresponding source change that would explain the diff on regen.
If you think you need an exception, the right move is to make the source
change explicit instead: either edit the wiki TOMLs / JSONs that feed the
generators, or edit MIGRATION_SQL / systems-schema.sql directly. There is
no hand-edit path that survives regen.
Savegame migration lineage (Phase 5+)
meta.schema_version now stores a monotonic semver string (T-888). When the savegame
system is built (Phase 5+), a save file records its schema_version string; the
loader can determine which migrations to apply by comparing that version to the
current one. meta.schema_sha retains the old SHA-1 for tamper detection.
When to bump SCHEMA_VERSION: edit the SCHEMA_VERSION = "1.0.0" constant in
tooling/schema_version.py whenever a schema change is backwards-incompatible
(column removed, type changed, FK constraint added, table dropped). Additive changes
(new nullable columns, new tables, new indexes) do not require a bump.