diff --git a/.claude/rules/asset-pipeline.md b/.claude/rules/asset-pipeline.md index 24289ebf1..280bd1ffc 100644 --- a/.claude/rules/asset-pipeline.md +++ b/.claude/rules/asset-pipeline.md @@ -23,8 +23,8 @@ 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` | -| `generate_atlas` | `python3 tooling/planet-gen/generate_atlas.py --seed 42` | `tooling/planet-gen/generate_atlas.py` | +| `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` | `import_economics` shells out to the Rust `generate_brands` binary as its first step to refresh `wiki/economics/corporations/generated_brands.toml`, then reads diff --git a/server/data/systems.db b/server/data/systems.db index ede8b6a83..d57fd208b 100644 Binary files a/server/data/systems.db and b/server/data/systems.db differ diff --git a/tooling/check-systems-db-stamp b/tooling/check-systems-db-stamp index 095f8542f..db985b8e8 100644 --- a/tooling/check-systems-db-stamp +++ b/tooling/check-systems-db-stamp @@ -45,9 +45,11 @@ GENERATOR_SOURCES: dict[str, list[Path]] = { REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "main.rs", REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "names.rs", REPO_ROOT / "tooling" / "generate-brands", + REPO_ROOT / "tooling" / "schema_version.py", ], "generate_atlas": [ REPO_ROOT / "tooling" / "planet-gen" / "generate_atlas.py", + REPO_ROOT / "tooling" / "schema_version.py", ], } @@ -96,7 +98,9 @@ def check(verbose: bool = False) -> int: stale: list[str] = [] unknown: list[str] = [] bad_version: list[str] = [] + seen_versions: dict[str, str] = {} # generator_name -> schema_version for generator_name, schema_version, stored_sha in rows: + seen_versions[generator_name] = schema_version # Validate schema_version is a semver string (#888). # Old DBs may still carry a SHA-1 hex (40-char) — flag them as stale # so the user knows to run make regen-db rather than getting a silent pass. @@ -137,6 +141,19 @@ def check(verbose: bool = False) -> int: print(f"check-systems-db-stamp: BAD schema_version — {msg}", file=sys.stderr) return 1 + # All generators must agree on the same schema_version (#888 defense-in-depth). + # If they differ, the DB was partially regenerated with different source trees. + unique_versions = set(seen_versions.values()) + if len(unique_versions) > 1: + print( + "check-systems-db-stamp: CONFLICT — generators disagree on schema_version:", + file=sys.stderr, + ) + for gen, ver in sorted(seen_versions.items()): + print(f" {gen}: {ver}", file=sys.stderr) + print(" Run: make regen-db", file=sys.stderr) + return 1 + if unknown: print( "check-systems-db-stamp: UNKNOWN generator(s) in meta table: " diff --git a/tooling/db/decision b/tooling/db/decision index 82492f616..4c5260b14 100755 --- a/tooling/db/decision +++ b/tooling/db/decision @@ -1,8 +1,10 @@ #!/usr/bin/env bash # Decision ID management — claim, query, and validate decision IDs. # Usage: -# decision next [D|Q|R] Show next available ID -# decision claim [title] Claim next ID (reserves in DB) -# decision check-dupes Check for duplicate IDs in markdown -# decision sync Sync markdown -> DB +# decision sync Sync decisions/*.md into SQLite +# decision show Show a decision with linked tickets + refs +# decision next [D|Q|R] Show next available ID +# decision claim [title] Claim next ID (reserves in DB) +# decision check-dupes Check for duplicate IDs in markdown +# decision orphan-tickets List tickets with invalid/missing decision_ref exec python3 "$(dirname "$0")/decisions_sync.py" "$@" diff --git a/tooling/db/decisions_sync.py b/tooling/db/decisions_sync.py index efd622c7c..c85d823a9 100644 --- a/tooling/db/decisions_sync.py +++ b/tooling/db/decisions_sync.py @@ -310,16 +310,14 @@ def sync(cfg): ) continue - try: - conn.execute( - """INSERT OR IGNORE INTO decision_refs - (source_id, target_id, ref_type, note) - VALUES (?, ?, ?, ?)""", - (d["id"], target_id, ref_type, note), - ) + cur = conn.execute( + """INSERT OR IGNORE INTO decision_refs + (source_id, target_id, ref_type, note) + VALUES (?, ?, ?, ?)""", + (d["id"], target_id, ref_type, note), + ) + if cur.rowcount > 0: refs_created += 1 - except sqlite3.IntegrityError: - pass # duplicate ref, skip conn.commit() diff --git a/tooling/economy-db/import_economics.py b/tooling/economy-db/import_economics.py index 035be62e0..67b8512d0 100755 --- a/tooling/economy-db/import_economics.py +++ b/tooling/economy-db/import_economics.py @@ -34,6 +34,10 @@ from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent.parent +# Import shared schema version constant (#888) — single source of truth in tooling/schema_version.py +sys.path.insert(0, str(REPO_ROOT / "tooling")) +from schema_version import SCHEMA_VERSION # noqa: E402 + DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" STAR_MAP = REPO_ROOT / "docs" / "design" / "star-map.json" COMMODITIES_TOML = REPO_ROOT / "wiki" / "economics" / "commodities.toml" @@ -51,13 +55,6 @@ GENERATE_BRANDS_NAMES_RS = REPO_ROOT / "server" / "src" / "bin" / "generate_bran GENERATE_BRANDS_WRAPPER = REPO_ROOT / "tooling" / "generate-brands" -# Monotonic schema version — bump manually on any backwards-incompatible schema change. -# Stored in meta.schema_version so future savegame migration lineage can order snapshots. -# (SHA-1 hashes cannot be ordered; semver can.) The SHA is preserved in meta.schema_sha -# for tamper detection alongside the semver (#888). -SCHEMA_VERSION = "1.0.0" - - def _file_sha1(*paths: Path) -> str: """Return SHA-1 hex of the concatenated content of one or more files. @@ -83,6 +80,7 @@ IMPORT_ECONOMICS_SOURCES: tuple[Path, ...] = ( GENERATE_BRANDS_RS, GENERATE_BRANDS_NAMES_RS, GENERATE_BRANDS_WRAPPER, + REPO_ROOT / "tooling" / "schema_version.py", ) diff --git a/tooling/planet-gen/generate_atlas.py b/tooling/planet-gen/generate_atlas.py index 3b8ec9a58..4689e3b6b 100644 --- a/tooling/planet-gen/generate_atlas.py +++ b/tooling/planet-gen/generate_atlas.py @@ -49,6 +49,10 @@ import yaml from planet_simulation import simulate +# Import shared schema version constant (#888) — single source of truth in tooling/schema_version.py +sys.path.insert(0, str(REPO_ROOT / "tooling")) +from schema_version import SCHEMA_VERSION # noqa: E402 + # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- @@ -92,11 +96,6 @@ _ATLAS_SCHEMA_END_MARKER = "-- END ATLAS INDEX" # Generator metadata stamp (#855, #856) # --------------------------------------------------------------------------- -# Monotonic schema version — shared constant with import_economics.py (#888). -# Bump manually on any backwards-incompatible schema change. -SCHEMA_VERSION = "1.0.0" - - def _file_sha1(*paths: Path) -> str: """Return SHA-1 hex of the concatenated content of one or more files. @@ -126,7 +125,7 @@ def _write_stamp(conn: sqlite3.Connection) -> None: a double-commit with the atlas data write that precedes it. """ schema_sha = _file_sha1(SYSTEMS_SCHEMA_PATH) - generator_sha = _file_sha1(Path(__file__)) + generator_sha = _file_sha1(Path(__file__), REPO_ROOT / "tooling" / "schema_version.py") conn.execute( """INSERT OR REPLACE INTO meta (generator_name, schema_version, schema_sha, generator_sha, generated_at) @@ -186,8 +185,9 @@ def ensure_atlas_schema(conn: sqlite3.Connection) -> None: # Add schema_sha column to existing DBs that pre-date #888 (#888 migration). try: conn.execute("ALTER TABLE meta ADD COLUMN schema_sha TEXT") - except Exception: - pass # column already exists + except sqlite3.OperationalError as e: + if "duplicate column" not in str(e).lower(): + raise def _first_int(values, default: int = 0) -> int: diff --git a/tooling/schema_version.py b/tooling/schema_version.py new file mode 100644 index 000000000..c6282d3c9 --- /dev/null +++ b/tooling/schema_version.py @@ -0,0 +1,14 @@ +""" +Canonical systems.db schema version — shared by all generators (#888). + +Bump SCHEMA_VERSION manually on any backwards-incompatible schema change +(column removed, type changed, FK constraint added, table dropped). +Additive changes (new nullable columns, new tables, new indexes) do not +require a bump. + +Imported by: + tooling/economy-db/import_economics.py + tooling/planet-gen/generate_atlas.py +""" + +SCHEMA_VERSION = "1.0.0"