The sole generator of systems.db moves to tooling/domains/ledger/ and is now `reach ledger import`. economy_import/ keeps its name (Rust comments in server/src cite it); the entrypoint becomes service.py; schema_version.py moves with the importer, which is where the version is defined. The stamp survived the move, which is the thing that had to hold: - generated_brands.toml is byte-identical (sha256 e748531…) before and after - `reach check systems-db-stamp` reported STALE after the move (the registry saw it) and OK after the regen - the dry-run carries every count and warning of the baseline transcript, and exit 2 — imported and stamped, coverage gate unmet — still reaches the caller through @command `make regen-db` survives as a one-line delegate, per D-263's muscle-memory clause: about fifty files name it, including the headers of generated wiki TOMLs and the remedies the push gate prints. `make economy-db` is retired; it ran `reach generate brands` before the import, which the import already does as its first step. economy_import.errors is reconciled as DOMAINS.md asked. ImportAborted stays as internal rollback control flow and never reaches a caller; the service converts it to a ReachError carrying the remedy. regenerate_brands caught cargo_binary's ReachError, printed it and raised ImportAborted, dropping the remedy. It runs before the import transaction opens, so there is nothing to roll back — it now propagates. Both sys.path bootstraps are gone; they existed only because the directory was hyphenated. The step labels ran [1/10]…[10/13]…[17/19]; one 24-step counter now drives the event phase and progress. Stale pointers fixed on the way: MIGRATION_SQL has lived in economy_import/migration.py since T-1067, but the asset-pipeline rule, DEVOPS and the schema comments still sent readers to import_economics.py; the rule and DEVOPS also still named the check scripts T-1281 retired. Co-Authored-By: Claude Opus 5.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
|
|
]
|