import_economics.py 2,620 → 309 lines — a thin orchestrator keeping the exact CLI, single-transaction/rollback contract, and exit codes. The 16 import steps, MIGRATION_SQL, brands shell-out, validators, and stamp write now live in tooling/economy-db/economy_import/ (db, migration, economy, corporations, brands, bodies, atlas, specialization, traits, validators, stamp, paths, errors). Full type hints throughout. tooling/generator_sources.py replaces the triplicated source registry (importer / stamp checker / pr-process watch list — the skill now derives its list via --list). The registry stamps itself, and economy_import/ modules are globbed fail-closed, so a future module is stamped the moment it exists — closing the silently-weakened-stamp failure mode. Rider: connector config helpers centralized in tooling/db/common.py. Byte-identical behavior proven: full-import table dump diff EMPTY over 107,843 lines / 37 tables (volatile timestamp fields excluded); dry-run output parity; generated_brands.toml sha unchanged. make test-tooling PASS; ruff clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
5.0 KiB
Python
138 lines
5.0 KiB
Python
"""Structural-integrity and D-175 coverage validation.
|
|
|
|
``validate`` is a pre-commit hard blocker; the coverage validators run after
|
|
commit (Phase 2 gate, exit 2) — see the entrypoint's main() for the contract.
|
|
"""
|
|
|
|
import sqlite3
|
|
|
|
|
|
def validate(conn: sqlite3.Connection) -> list[str]:
|
|
"""Validate structural integrity of imported data.
|
|
|
|
Checks FK integrity, chain commodity references, and chain completeness.
|
|
These are hard blockers — broken data must not be committed.
|
|
|
|
Coverage validation (commodity/system thresholds) is separate and runs
|
|
after commit via validate_commodity_coverage() and validate_system_coverage().
|
|
"""
|
|
errors: list[str] = []
|
|
|
|
# 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}'")
|
|
|
|
# economic_role must be one of the D-194 canonical 10 values
|
|
valid_roles = {
|
|
'manufacturing', 'financial', 'agricultural', 'extraction',
|
|
'service_mixed', 'institutional', 'transit_hub', 'research',
|
|
'military', 'residential',
|
|
}
|
|
bad_roles = conn.execute("""
|
|
SELECT DISTINCT economic_role, COUNT(*) as cnt
|
|
FROM bodies
|
|
WHERE economic_role IS NOT NULL
|
|
AND economic_role NOT IN (
|
|
'manufacturing', 'financial', 'agricultural', 'extraction',
|
|
'service_mixed', 'institutional', 'transit_hub', 'research',
|
|
'military', 'residential'
|
|
)
|
|
GROUP BY economic_role
|
|
""").fetchall()
|
|
for role, cnt in bad_roles:
|
|
errors.append(
|
|
f"bodies.economic_role: non-canonical value '{role}' on {cnt} row(s) — "
|
|
f"valid values: {sorted(valid_roles)}"
|
|
)
|
|
|
|
# Chain completeness: every intermediate commodity must have at least one producer
|
|
missing_chains = conn.execute("""
|
|
SELECT c.commodity_id, c.name
|
|
FROM commodities c
|
|
WHERE c.tier = 'intermediate'
|
|
AND c.commodity_id NOT IN (SELECT output_commodity_id FROM production_chains)
|
|
ORDER BY c.commodity_id
|
|
""").fetchall()
|
|
for cid, name in missing_chains:
|
|
errors.append(f"chain completeness: no production chain produces intermediate '{cid}' ({name})")
|
|
|
|
return errors
|
|
|
|
|
|
def validate_commodity_coverage(
|
|
conn: sqlite3.Connection, wiki_corps: list[dict], commodity_ids: set[str]
|
|
) -> list[str]:
|
|
"""3+ corporations per major commodity type (raw + intermediate). D-175."""
|
|
errors: list[str] = []
|
|
major = [
|
|
r[0]
|
|
for r in conn.execute(
|
|
"SELECT commodity_id FROM commodities "
|
|
"WHERE tier IN ('raw', 'intermediate') ORDER BY commodity_id"
|
|
).fetchall()
|
|
]
|
|
|
|
# Build commodity → corp set from wiki tags filtered to known commodity IDs
|
|
coverage: dict[str, set[str]] = {cid: set() for cid in major}
|
|
for corp in wiki_corps:
|
|
for tag in corp.get("tags", []):
|
|
if tag in coverage:
|
|
coverage[tag].add(corp["corp_id"])
|
|
|
|
for cid in major:
|
|
n = len(coverage[cid])
|
|
if n < 3:
|
|
corp_list = sorted(coverage[cid]) if coverage[cid] else ["none"]
|
|
errors.append(
|
|
f"commodity coverage: '{cid}' has {n}/3 corp(s) — {corp_list}"
|
|
)
|
|
|
|
return errors
|
|
|
|
|
|
def validate_system_coverage(
|
|
conn: sqlite3.Connection, wiki_corps: list[dict]
|
|
) -> list[str]:
|
|
"""1+ corporation per inhabited system with population > 100K. D-175.
|
|
|
|
Uses wiki_corps headquarters data (not DB corp_presence) so this check
|
|
is accurate in both dry-run and real-run modes.
|
|
"""
|
|
covered = {c["system_id"] for c in wiki_corps if c.get("system_id")}
|
|
populated = conn.execute("""
|
|
SELECT se.system_id, ss.proper_name, se.population
|
|
FROM system_economy se
|
|
JOIN star_systems ss ON se.system_id = ss.system_id
|
|
WHERE se.population > 100000
|
|
ORDER BY se.system_id
|
|
""").fetchall()
|
|
|
|
return [
|
|
f"system coverage: no corp presence in '{sid}' ({name}, pop={pop:,})"
|
|
for sid, name, pop in populated
|
|
if sid not in covered
|
|
]
|