From 571a708d4a505ea6710c6824b238567b42ee44e8 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 6 Apr 2026 23:32:32 +0200 Subject: [PATCH] feat(schema): extend systems.db for economics (#804) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 5 new tables: gate_links (668 bidirectional edges from star-map.json), commodities (36 types from commodities.toml), production_chains (21 Leontief recipes), chain_inputs (52 input requirements), corp_presence (empty, populated by future pipeline). Add currency_zone column to star_systems (D-172), archetype columns to corporations (D-175). New import pipeline: tooling/economy-db/import_economics.py reads TOML/JSON source files and populates the DB. Idempotent — safe to rerun via `make economy-db`. Co-Authored-By: Claude Opus 4.6 --- Makefile | 6 +- server/data/systems-schema.sql | 75 +++++ server/data/systems.db | Bin 1036288 -> 1110016 bytes tooling/economy-db/import_economics.py | 388 +++++++++++++++++++++++++ 4 files changed, 468 insertions(+), 1 deletion(-) create mode 100755 tooling/economy-db/import_economics.py diff --git a/Makefile b/Makefile index d4a2a11c0..42189b692 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ 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 \ decisions-sync decisions-coverage decisions-active decisions-orphan \ db-backup db-install validate-content check-fact-ids setup-hooks \ - audit atlas-verify \ + audit atlas-verify economy-db \ pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \ pre-pr-server pre-pr-client pre-pr-content \ fixtures-client fixtures-gauntlet golden-diff golden-update \ @@ -53,6 +53,7 @@ help: @echo " make validate-content Validate content YAML against schemas" @echo " make check-fact-ids Check fact_id references against knowledge catalogs" @echo " make atlas-verify Verify atlas proposal JSONs (all in docs/atlas/proposals/)" + @echo " make economy-db Import economics data into systems.db (TOML/JSON → SQLite)" @echo " make fixtures-client Generate GDScript->Rust cross-encoder fixtures (#475)" @echo " make golden-diff Show diff if golden file output has changed" @echo " make golden-update Regenerate golden file and stage for commit" @@ -323,6 +324,9 @@ db-backup: db-install: @tooling/db-install +economy-db: ## Import economics data (commodities, chains, gate links) into systems.db + @python3 tooling/economy-db/import_economics.py + # --- Decisions --- decisions-sync: diff --git a/server/data/systems-schema.sql b/server/data/systems-schema.sql index d02ad559e..ad94690b7 100644 --- a/server/data/systems-schema.sql +++ b/server/data/systems-schema.sql @@ -38,6 +38,9 @@ CREATE TABLE IF NOT EXISTS star_systems ( cultural_corridor TEXT, generation_priority TEXT, + -- Economics (D-172) + currency_zone TEXT DEFAULT 'TRACTUS_PRIMARY', -- TRACTUS_PRIMARY | MARK_PRIMARY | MIXED + updated_at TEXT DEFAULT (datetime('now')) ); @@ -201,9 +204,73 @@ CREATE TABLE IF NOT EXISTS corporations ( specialization TEXT, -- primary product/service parent_corp TEXT REFERENCES corporations(corp_id), notes TEXT, + + -- Economics (D-175) + behavioral_archetype TEXT, -- Monopolist|Distributor|Producer|Specialist|Cooperative|Intermediary + supply_chain_role TEXT, + shadow_economy_access INTEGER DEFAULT 0, + updated_at TEXT DEFAULT (datetime('now')) ); +-- Gate adjacency graph — bidirectional edges from star-map.json (D-178) +-- Transport cost computation: 5–12% per hop (gate edge), 1–3% (orbital edge) +CREATE TABLE IF NOT EXISTS gate_links ( + from_system_id TEXT NOT NULL REFERENCES star_systems(system_id), + to_system_id TEXT NOT NULL REFERENCES star_systems(system_id), + PRIMARY KEY (from_system_id, to_system_id) +); + +-- Commodity catalog — 36 types (D-184) +-- Source: wiki/economics/commodities.toml +CREATE TABLE IF NOT EXISTS commodities ( + commodity_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + tier TEXT NOT NULL, -- raw|intermediate|final|service_professional|service_luxury + elasticity TEXT NOT NULL, -- perfectly_inelastic|inelastic|unit_elastic|elastic + base_price REAL NOT NULL, + bulk_class TEXT, -- bulk|liquid|perishable|standard|compact|precision|oversized|non_physical + unit TEXT, -- tonnes|units|contracts + production_ubiquity TEXT, -- ubiquitous|common|regional|concentrated|monopolistic + demand_model TEXT, -- market|utility|compliance + commission_certifiable INTEGER DEFAULT 0, -- subject to certification in TRACTUS_PRIMARY zones + compact_contested INTEGER DEFAULT 0, + shadow_viable INTEGER DEFAULT 0, + panic_threshold_weeks INTEGER DEFAULT 0, + description TEXT, + updated_at TEXT DEFAULT (datetime('now')) +); + +-- Leontief production chain recipes (D-178) +-- Source: wiki/economics/production_chains.toml +CREATE TABLE IF NOT EXISTS production_chains ( + chain_id TEXT PRIMARY KEY, + output_commodity_id TEXT NOT NULL REFERENCES commodities(commodity_id), + output_quantity REAL NOT NULL DEFAULT 1.0, + location_bound INTEGER DEFAULT 0, + description TEXT, + updated_at TEXT DEFAULT (datetime('now')) +); + +-- Input requirements per production chain +CREATE TABLE IF NOT EXISTS chain_inputs ( + chain_id TEXT NOT NULL REFERENCES production_chains(chain_id), + input_commodity_id TEXT NOT NULL REFERENCES commodities(commodity_id), + quantity REAL NOT NULL, + PRIMARY KEY (chain_id, input_commodity_id) +); + +-- Corporation operating locations (D-175) +-- Populated by corporation assignment pipeline, not by initial import +CREATE TABLE IF NOT EXISTS corp_presence ( + corp_id TEXT NOT NULL REFERENCES corporations(corp_id), + location_id TEXT NOT NULL, -- body_id or station_id + location_type TEXT NOT NULL, -- body|station + primary_operation TEXT, + updated_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (corp_id, location_id) +); + -- Indexes -- astronomical_id removed: system_id IS the GJ catalog number CREATE INDEX IF NOT EXISTS idx_star_systems_sector ON star_systems(geographic_sector); @@ -224,3 +291,11 @@ CREATE INDEX IF NOT EXISTS idx_stations_orbits ON stations(orbits_body_id); CREATE INDEX IF NOT EXISTS idx_stations_type ON stations(station_type); CREATE INDEX IF NOT EXISTS idx_corps_system ON corporations(headquarters_system); CREATE INDEX IF NOT EXISTS idx_corps_type ON corporations(corp_type); +CREATE INDEX IF NOT EXISTS idx_star_systems_currency ON star_systems(currency_zone); +CREATE INDEX IF NOT EXISTS idx_gate_links_from ON gate_links(from_system_id); +CREATE INDEX IF NOT EXISTS idx_gate_links_to ON gate_links(to_system_id); +CREATE INDEX IF NOT EXISTS idx_commodities_tier ON commodities(tier); +CREATE INDEX IF NOT EXISTS idx_production_chains_output ON production_chains(output_commodity_id); +CREATE INDEX IF NOT EXISTS idx_chain_inputs_commodity ON chain_inputs(input_commodity_id); +CREATE INDEX IF NOT EXISTS idx_corp_presence_corp ON corp_presence(corp_id); +CREATE INDEX IF NOT EXISTS idx_corp_presence_location ON corp_presence(location_id); diff --git a/server/data/systems.db b/server/data/systems.db index 632e87bda454d0ff220b74821f2f2d98219176a6..bb667d19db275157251262fe645aa9f1aee92f25 100644 GIT binary patch delta 77051 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 Xi1Q}vTHens?-wZV7u?Y=wDNxe!WDsJ diff --git a/tooling/economy-db/import_economics.py b/tooling/economy-db/import_economics.py new file mode 100755 index 000000000..d4d3e3c82 --- /dev/null +++ b/tooling/economy-db/import_economics.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +""" +Import economics data into systems.db. + +Reads TOML/JSON source files and populates the economics tables: + - gate_links from docs/design/star-map.json (335 edges, bidirectional) + - commodities from wiki/economics/commodities.toml (36 types) + - production_chains + chain_inputs from wiki/economics/production_chains.toml + - currency_zone on star_systems (default TRACTUS_PRIMARY) + +Does NOT populate corp_presence — that's a future pipeline step. + +Usage: + python3 tooling/economy-db/import_economics.py + python3 tooling/economy-db/import_economics.py --dry-run + python3 tooling/economy-db/import_economics.py --db path/to/systems.db +""" + +import argparse +import json +import os +import sqlite3 +import sys +import tomllib +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent + +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" +CHAINS_TOML = REPO_ROOT / "wiki" / "economics" / "production_chains.toml" +SCHEMA_SQL = REPO_ROOT / "server" / "data" / "systems-schema.sql" + + +# --------------------------------------------------------------------------- +# Schema migration — add new tables and columns to existing DB +# --------------------------------------------------------------------------- + +MIGRATION_SQL = """ +-- Economics tables (idempotent — safe to re-run) + +CREATE TABLE IF NOT EXISTS gate_links ( + from_system_id TEXT NOT NULL REFERENCES star_systems(system_id), + to_system_id TEXT NOT NULL REFERENCES star_systems(system_id), + PRIMARY KEY (from_system_id, to_system_id) +); + +CREATE TABLE IF NOT EXISTS commodities ( + commodity_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + tier TEXT NOT NULL, + elasticity TEXT NOT NULL, + base_price REAL NOT NULL, + bulk_class TEXT, + unit TEXT, + production_ubiquity TEXT, + demand_model TEXT, + commission_certifiable INTEGER DEFAULT 0, + compact_contested INTEGER DEFAULT 0, + shadow_viable INTEGER DEFAULT 0, + panic_threshold_weeks INTEGER DEFAULT 0, + description TEXT, + updated_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS production_chains ( + chain_id TEXT PRIMARY KEY, + output_commodity_id TEXT NOT NULL REFERENCES commodities(commodity_id), + output_quantity REAL NOT NULL DEFAULT 1.0, + location_bound INTEGER DEFAULT 0, + description TEXT, + updated_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS chain_inputs ( + chain_id TEXT NOT NULL REFERENCES production_chains(chain_id), + input_commodity_id TEXT NOT NULL REFERENCES commodities(commodity_id), + quantity REAL NOT NULL, + PRIMARY KEY (chain_id, input_commodity_id) +); + +CREATE TABLE IF NOT EXISTS corp_presence ( + corp_id TEXT NOT NULL REFERENCES corporations(corp_id), + location_id TEXT NOT NULL, + location_type TEXT NOT NULL, + primary_operation TEXT, + updated_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (corp_id, location_id) +); + +-- New indexes +CREATE INDEX IF NOT EXISTS idx_star_systems_currency ON star_systems(currency_zone); +CREATE INDEX IF NOT EXISTS idx_gate_links_from ON gate_links(from_system_id); +CREATE INDEX IF NOT EXISTS idx_gate_links_to ON gate_links(to_system_id); +CREATE INDEX IF NOT EXISTS idx_commodities_tier ON commodities(tier); +CREATE INDEX IF NOT EXISTS idx_production_chains_output ON production_chains(output_commodity_id); +CREATE INDEX IF NOT EXISTS idx_chain_inputs_commodity ON chain_inputs(input_commodity_id); +CREATE INDEX IF NOT EXISTS idx_corp_presence_corp ON corp_presence(corp_id); +CREATE INDEX IF NOT EXISTS idx_corp_presence_location ON corp_presence(location_id); +""" + +# Columns to add to existing tables (ALTER TABLE is idempotent via try/except) +COLUMN_MIGRATIONS = [ + ("star_systems", "currency_zone", "TEXT DEFAULT 'TRACTUS_PRIMARY'"), + ("corporations", "behavioral_archetype", "TEXT"), + ("corporations", "supply_chain_role", "TEXT"), + ("corporations", "shadow_economy_access", "INTEGER DEFAULT 0"), +] + + +def _add_column(conn: sqlite3.Connection, table: str, col: str, col_type: str): + """Add a column if it doesn't exist. SQLite has no IF NOT EXISTS for ALTER.""" + try: + conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {col_type}") + except sqlite3.OperationalError as e: + if "duplicate column" in str(e).lower(): + pass # already exists + else: + raise + + +# --------------------------------------------------------------------------- +# Gate links +# --------------------------------------------------------------------------- + +def import_gate_links(conn: sqlite3.Connection, dry_run: bool) -> int: + with open(STAR_MAP) as f: + data = json.load(f) + + edges = data["edges"] + system_ids = {r[0] for r in conn.execute("SELECT system_id FROM star_systems").fetchall()} + + rows = [] + skipped = [] + for a, b in edges: + if a not in system_ids: + skipped.append(a) + continue + if b not in system_ids: + skipped.append(b) + continue + rows.append((a, b)) + rows.append((b, a)) + + if skipped: + unique_skipped = sorted(set(skipped)) + print(f" warning: {len(unique_skipped)} system(s) in star-map.json not in DB: {unique_skipped[:5]}...") + + if not dry_run: + conn.executemany( + "INSERT OR IGNORE INTO gate_links (from_system_id, to_system_id) VALUES (?, ?)", + rows, + ) + + return len(rows) + + +# --------------------------------------------------------------------------- +# Commodities +# --------------------------------------------------------------------------- + +def import_commodities(conn: sqlite3.Connection, dry_run: bool) -> int: + with open(COMMODITIES_TOML, "rb") as f: + data = tomllib.load(f) + + rows = [] + for cid, c in data.items(): + rows.append(( + cid, + c["name"], + c["tier"], + c["elasticity"], + c["base_price"], + c.get("bulk_class"), + c.get("unit"), + c.get("production_ubiquity"), + c.get("demand_model"), + int(c.get("commission_certifiable", False)), + int(c.get("compact_contested", False)), + int(c.get("shadow_viable", False)), + c.get("panic_threshold_weeks", 0), + c.get("description"), + )) + + if not dry_run: + conn.executemany( + """INSERT INTO commodities ( + commodity_id, name, tier, elasticity, base_price, + bulk_class, unit, production_ubiquity, demand_model, + commission_certifiable, compact_contested, shadow_viable, + panic_threshold_weeks, description + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + rows, + ) + + return len(rows) + + +# --------------------------------------------------------------------------- +# Production chains + inputs +# --------------------------------------------------------------------------- + +def import_chains(conn: sqlite3.Connection, dry_run: bool) -> tuple[int, int]: + with open(CHAINS_TOML, "rb") as f: + data = tomllib.load(f) + + chain_rows = [] + input_rows = [] + for chain_id, c in data.items(): + chain_rows.append(( + chain_id, + c["output"], + c.get("output_quantity", 1.0), + int(c.get("location_bound", False)), + c.get("description"), + )) + for inp in c.get("inputs", []): + input_rows.append(( + chain_id, + inp["commodity"], + inp["quantity"], + )) + + if not dry_run: + conn.executemany( + """INSERT INTO production_chains ( + chain_id, output_commodity_id, output_quantity, + location_bound, description + ) VALUES (?, ?, ?, ?, ?)""", + chain_rows, + ) + conn.executemany( + """INSERT INTO chain_inputs ( + chain_id, input_commodity_id, quantity + ) VALUES (?, ?, ?)""", + input_rows, + ) + + return len(chain_rows), len(input_rows) + + +# --------------------------------------------------------------------------- +# Currency zones +# --------------------------------------------------------------------------- + +def set_currency_zones(conn: sqlite3.Connection, dry_run: bool) -> dict: + """Set currency_zone on star_systems. Default TRACTUS_PRIMARY, Sol = MIXED.""" + if dry_run: + return {"TRACTUS_PRIMARY": "all", "MIXED": "GJ 0"} + + # Default everything to TRACTUS_PRIMARY + conn.execute("UPDATE star_systems SET currency_zone = 'TRACTUS_PRIMARY' WHERE currency_zone IS NULL") + + # Sol system is MIXED (Earth legacy currency presence) + conn.execute("UPDATE star_systems SET currency_zone = 'MIXED' WHERE system_id = 'GJ 0'") + + # Future: Compact systems → MARK_PRIMARY (requires authored Compact membership data) + + counts = {} + for row in conn.execute("SELECT currency_zone, COUNT(*) FROM star_systems GROUP BY currency_zone"): + counts[row[0]] = row[1] + + return counts + + +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- + +def validate(conn: sqlite3.Connection) -> list[str]: + errors = [] + + # FK integrity + fk_issues = conn.execute("PRAGMA foreign_key_check").fetchall() + if fk_issues: + for issue in fk_issues[:10]: + errors.append(f"FK violation: table={issue[0]} rowid={issue[1]} " + f"parent={issue[2]} fkid={issue[3]}") + + # Chain inputs reference valid commodities + orphan_inputs = conn.execute(""" + SELECT ci.chain_id, ci.input_commodity_id + FROM chain_inputs ci + LEFT JOIN commodities c ON ci.input_commodity_id = c.commodity_id + WHERE c.commodity_id IS NULL + """).fetchall() + for chain_id, cid in orphan_inputs: + errors.append(f"chain_inputs: chain '{chain_id}' references unknown commodity '{cid}'") + + # Chain outputs reference valid commodities + orphan_outputs = conn.execute(""" + SELECT pc.chain_id, pc.output_commodity_id + FROM production_chains pc + LEFT JOIN commodities c ON pc.output_commodity_id = c.commodity_id + WHERE c.commodity_id IS NULL + """).fetchall() + for chain_id, cid in orphan_outputs: + errors.append(f"production_chains: chain '{chain_id}' outputs unknown commodity '{cid}'") + + return errors + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description="Import economics data into systems.db") + parser.add_argument("--db", default=str(DB_PATH), help="Path to systems.db") + parser.add_argument("--dry-run", action="store_true", help="Validate without writing") + args = parser.parse_args() + + db_path = Path(args.db) + if not db_path.exists(): + print(f"error: {db_path} not found", file=sys.stderr) + sys.exit(1) + + print(f"\n Economics Import Pipeline") + print(f" DB: {db_path}") + if args.dry_run: + print(f" Mode: DRY RUN") + print() + + conn = sqlite3.connect(str(db_path)) + conn.execute("PRAGMA foreign_keys=ON") + + # 1. Migrate schema + print(" [1/5] Schema migration...") + for table, col, col_type in COLUMN_MIGRATIONS: + _add_column(conn, table, col, col_type) + conn.executescript(MIGRATION_SQL) + print(" tables and columns ready") + + # Clear economics tables in FK-safe order (children before parents) + if not args.dry_run: + conn.execute("DELETE FROM chain_inputs") + conn.execute("DELETE FROM production_chains") + conn.execute("DELETE FROM commodities") + conn.execute("DELETE FROM gate_links") + + # 2. Gate links + print(" [2/5] Importing gate links...") + n_links = import_gate_links(conn, args.dry_run) + print(f" {n_links} rows (bidirectional)") + + # 3. Commodities + print(" [3/5] Importing commodities...") + n_commodities = import_commodities(conn, args.dry_run) + print(f" {n_commodities} commodities") + + # 4. Production chains + print(" [4/5] Importing production chains...") + n_chains, n_inputs = import_chains(conn, args.dry_run) + print(f" {n_chains} chains, {n_inputs} inputs") + + # 5. Currency zones + print(" [5/5] Setting currency zones...") + zones = set_currency_zones(conn, args.dry_run) + for zone, count in sorted(zones.items()): + print(f" {zone}: {count}") + + # Validate + print("\n Validating...") + errors = validate(conn) + if errors: + print(f" ERRORS ({len(errors)}):") + for e in errors: + print(f" - {e}") + conn.close() + sys.exit(1) + else: + print(" FK integrity OK") + + if not args.dry_run: + conn.commit() + print("\n Committed.") + else: + print("\n Dry run — no changes written.") + + conn.close() + + print(f"\n Done: {n_links} gate_links, {n_commodities} commodities, " + f"{n_chains} chains, {n_inputs} inputs\n") + + +if __name__ == "__main__": + main()