Files
settled-reach/tooling/check-systems-db-stamp
T
jpmschweitzerandClaude Opus 4.6 91066e4be6 chore(db): expand stamp GENERATOR_SOURCES for naming helpers (#892)
Add gemma_naming.py and naming_core.py to generate_atlas source
tracking in check-systems-db-stamp. Mirror in pr-push watch list.
Prevents naming helper changes from bypassing stale-stamp detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-02 18:40:12 +02:00

159 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""
check-systems-db-stamp — verify that server/data/systems.db is up to date.
Reads the meta table from systems.db and checks that the stored SHA-1 of each
generator's source file(s) matches the current file content on disk.
Exit codes:
0 — DB is stamped and all generator SHAs match current sources
1 — DB is stale, has an unknown generator, or references a missing source file
2 — DB does not have a meta table (treat as unstamped — run make regen-db)
Usage (called by .config/hooks/pre-push):
tooling/check-systems-db-stamp
Usage (interactive):
tooling/check-systems-db-stamp --verbose
Decision refs: #855 (generator versioning), #857 (pre-push hook)
"""
import hashlib
import sqlite3
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
# Maps generator_name (as stored in meta.generator_name) to the source
# file(s) whose SHA is stamped. The SHA is computed as SHA-1 of the
# concatenated bytes of all files in sorted order.
#
# import_economics' source set includes the Rust generate_brands binary it now
# invokes as a subroutine (#136 review T2/H3). Keep this list in sync with
# IMPORT_ECONOMICS_SOURCES in tooling/economy-db/import_economics.py.
GENERATOR_SOURCES: dict[str, list[Path]] = {
"import_economics": [
REPO_ROOT / "tooling" / "economy-db" / "import_economics.py",
REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "main.rs",
REPO_ROOT / "server" / "src" / "bin" / "generate_brands" / "names.rs",
REPO_ROOT / "tooling" / "generate-brands",
],
"generate_atlas": [
REPO_ROOT / "tooling" / "planet-gen" / "generate_atlas.py",
REPO_ROOT / "tooling" / "planet-gen" / "gemma_naming.py",
REPO_ROOT / "tooling" / "planet-gen" / "naming_core.py",
REPO_ROOT / "tooling" / "planet-gen" / "import_heightmaps.py",
REPO_ROOT / "tooling" / "planet-gen" / "import_province_boundaries.py",
],
}
def file_sha1(*paths: Path) -> str:
"""SHA-1 of concatenated file contents (sorted paths).
Missing files raise FileNotFoundError rather than silently contributing
an empty-string hash (H2): a ghost SHA could mask real breakage when
stored and current SHAs converge on the empty-bytes digest.
"""
h = hashlib.sha1()
for p in sorted(paths):
if not p.exists():
raise FileNotFoundError(f"generator source not found: {p}")
h.update(p.read_bytes())
return h.hexdigest()
def check(verbose: bool = False) -> int:
"""Return exit code: 0 = fresh, 1 = stale, 2 = no meta table."""
if not DB_PATH.exists():
if verbose:
print(f"check-systems-db-stamp: {DB_PATH} not found — skipping check")
return 0
try:
conn = sqlite3.connect(str(DB_PATH))
rows = conn.execute(
"SELECT generator_name, generator_sha FROM meta"
).fetchall()
conn.close()
except sqlite3.OperationalError:
# meta table does not exist
if verbose:
print("check-systems-db-stamp: no meta table — systems.db has not been stamped")
print(" Run: make regen-db")
return 2
if not rows:
if verbose:
print("check-systems-db-stamp: meta table is empty — systems.db has not been stamped")
print(" Run: make regen-db")
return 2
stale: list[str] = []
unknown: list[str] = []
for generator_name, stored_sha in rows:
sources = GENERATOR_SOURCES.get(generator_name)
if sources is None:
# Unknown generator — fail closed (T6). A future branch adding a
# new generator without registering it here must update this map
# before the check will pass, preventing the "silent no-op" trap.
unknown.append(generator_name)
continue
try:
current_sha = file_sha1(*sources)
except FileNotFoundError as exc:
# Source file moved/deleted — explicit failure instead of
# silent empty-hash (H2).
print(
f"check-systems-db-stamp: BROKEN — {generator_name}: {exc}",
file=sys.stderr,
)
return 1
if current_sha != stored_sha:
stale.append(generator_name)
if verbose:
print(
f"check-systems-db-stamp: STALE — {generator_name}"
f"\n stored: {stored_sha}"
f"\n current: {current_sha}"
)
if unknown:
print(
"check-systems-db-stamp: UNKNOWN generator(s) in meta table: "
f"{unknown}",
file=sys.stderr,
)
print(
" Update GENERATOR_SOURCES in tooling/check-systems-db-stamp to "
"register them before pushing.",
file=sys.stderr,
)
return 1
if stale:
if not verbose:
print(
"systems.db is stale — run `make regen-db` before pushing.",
file=sys.stderr,
)
print(f" Stale generators: {stale}", file=sys.stderr)
return 1
if verbose:
print(f"check-systems-db-stamp: OK — {len(rows)} generator(s) up to date")
return 0
def main() -> None:
verbose = "--verbose" in sys.argv or "-v" in sys.argv
sys.exit(check(verbose=verbose))
if __name__ == "__main__":
main()