Merge remote-tracking branch 'origin/main' into sprint-38/server

# Conflicts:
#	CHANGELOG.md
#	server/data/systems.db
#	tooling/check-systems-db-stamp
#	tooling/economy-db/import_economics.py
#	tooling/planet-gen/generate_atlas.py
This commit is contained in:
2026-05-03 09:30:48 +02:00
30 changed files with 429 additions and 99 deletions
+38 -2
View File
@@ -20,10 +20,14 @@ Decision refs: #855 (generator versioning), #857 (pre-push hook)
"""
import hashlib
import re
import sqlite3
import sys
from pathlib import Path
# semver pattern: MAJOR.MINOR.PATCH (no pre-release or build metadata)
_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
REPO_ROOT = Path(__file__).resolve().parent.parent
DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
@@ -41,6 +45,7 @@ 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",
@@ -48,6 +53,7 @@ GENERATOR_SOURCES: dict[str, list[Path]] = {
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",
REPO_ROOT / "tooling" / "schema_version.py",
],
}
@@ -77,7 +83,7 @@ def check(verbose: bool = False) -> int:
try:
conn = sqlite3.connect(str(DB_PATH))
rows = conn.execute(
"SELECT generator_name, generator_sha FROM meta"
"SELECT generator_name, schema_version, generator_sha FROM meta"
).fetchall()
conn.close()
except sqlite3.OperationalError:
@@ -95,7 +101,19 @@ def check(verbose: bool = False) -> int:
stale: list[str] = []
unknown: list[str] = []
for generator_name, stored_sha in rows:
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.
if not _SEMVER_RE.match(schema_version or ""):
bad_version.append(
f"{generator_name}: schema_version='{schema_version}' "
f"(expected semver like '1.0.0' — run make regen-db)"
)
sources = GENERATOR_SOURCES.get(generator_name)
if sources is None:
# Unknown generator — fail closed (T6). A future branch adding a
@@ -122,6 +140,24 @@ def check(verbose: bool = False) -> int:
f"\n current: {current_sha}"
)
if bad_version:
for msg in bad_version:
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: "
+6 -4
View File
@@ -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 <D|Q|R> <domain> [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 <D-NNN> Show a decision with linked tickets + refs
# decision next [D|Q|R] Show next available ID
# decision claim <D|Q|R> <domain> [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" "$@"
+39 -9
View File
@@ -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()
@@ -457,6 +455,35 @@ def show_decision(cfg, decision_id):
conn.close()
def orphan_tickets(cfg):
"""List tickets whose decision_ref is set but does not match any decision in the DB."""
conn = get_connection(cfg)
try:
rows = conn.execute(
"""SELECT t.id, t.title, t.decision_ref, t.status, t.team
FROM tickets t
WHERE t.decision_ref IS NOT NULL
AND t.decision_ref != ''
AND t.decision_ref NOT IN (SELECT id FROM decisions)
ORDER BY t.decision_ref, t.id""",
).fetchall()
orphans = [dict(r) for r in rows]
return {
"ok": True,
"count": len(orphans),
"orphans": orphans,
"summary": (
f"{len(orphans)} orphan ticket(s) found"
if orphans
else "No orphan tickets — all decision_ref values are valid"
),
}
finally:
conn.close()
def check_dupes(cfg):
"""Check for duplicate decision IDs across all markdown files."""
# Pre-existing collisions too deeply embedded to renumber (139+ references).
@@ -508,6 +535,7 @@ Usage:
decisions_sync.py next [D|Q|R] Show next available ID (all prefixes or one)
decisions_sync.py claim <D|Q|R> <domain> [title] Claim next ID and insert placeholder
decisions_sync.py check-dupes Check for duplicate IDs across markdown files
decisions_sync.py orphan-tickets List tickets with invalid/missing decision_ref
decisions_sync.py --help Show this help message
ID claiming workflow:
@@ -554,6 +582,8 @@ def main():
result = claim_id(cfg, prefix, domain, title)
elif cmd == "check-dupes":
result = check_dupes(cfg)
elif cmd == "orphan-tickets":
result = orphan_tickets(cfg)
else:
result = {"ok": False, "error": f"Unknown command: {cmd}. Use --help for usage."}
+10 -3
View File
@@ -35,6 +35,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"
@@ -78,6 +82,7 @@ IMPORT_ECONOMICS_SOURCES: tuple[Path, ...] = (
GENERATE_BRANDS_RS,
GENERATE_BRANDS_NAMES_RS,
GENERATE_BRANDS_WRAPPER,
REPO_ROOT / "tooling" / "schema_version.py",
)
@@ -99,9 +104,10 @@ def _write_stamp(conn: sqlite3.Connection, generator_name: str, *source_files: P
schema_sha = _file_sha1(SCHEMA_SQL)
generator_sha = _file_sha1(*source_files)
conn.execute(
"""INSERT OR REPLACE INTO meta (generator_name, schema_version, generator_sha, generated_at)
VALUES (?, ?, ?, datetime('now'))""",
(generator_name, schema_sha, generator_sha),
"""INSERT OR REPLACE INTO meta
(generator_name, schema_version, schema_sha, generator_sha, generated_at)
VALUES (?, ?, ?, ?, datetime('now'))""",
(generator_name, SCHEMA_VERSION, schema_sha, generator_sha),
)
@@ -350,6 +356,7 @@ COLUMN_MIGRATIONS = [
("corporations", "shadow_economy_access", "INTEGER DEFAULT 0"),
("brand_products", "price_tier", "TEXT"),
("bodies", "body_radius_km", "REAL"), # D-204 — physical radius in km, nullable
("meta", "schema_sha", "TEXT"),
]
+15 -4
View File
@@ -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
# ---------------------------------------------------------------------------
@@ -121,7 +125,6 @@ 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)
# Hash all generate_atlas sources — must match GENERATOR_SOURCES in check-systems-db-stamp.
_atlas_dir = Path(__file__).parent
generator_sha = _file_sha1(
Path(__file__),
@@ -129,12 +132,13 @@ def _write_stamp(conn: sqlite3.Connection) -> None:
_atlas_dir / "naming_core.py",
_atlas_dir / "import_heightmaps.py",
_atlas_dir / "import_province_boundaries.py",
REPO_ROOT / "tooling" / "schema_version.py",
)
conn.execute(
"""INSERT OR REPLACE INTO meta
(generator_name, schema_version, generator_sha, generated_at)
VALUES ('generate_atlas', ?, ?, datetime('now'))""",
(schema_sha, generator_sha),
(generator_name, schema_version, schema_sha, generator_sha, generated_at)
VALUES ('generate_atlas', ?, ?, ?, datetime('now'))""",
(SCHEMA_VERSION, schema_sha, generator_sha),
)
@@ -180,11 +184,18 @@ def ensure_atlas_schema(conn: sqlite3.Connection) -> None:
CREATE TABLE IF NOT EXISTS meta (
generator_name TEXT PRIMARY KEY,
schema_version TEXT NOT NULL,
schema_sha TEXT,
generator_sha TEXT NOT NULL,
generated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
"""
)
# 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 sqlite3.OperationalError as e:
if "duplicate column" not in str(e).lower():
raise
def _first_int(values, default: int = 0) -> int:
+14
View File
@@ -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"