Files
settled-reach/tooling/domains/ledger/economy_import/validators.py
T
jpmschweitzerandClaude Opus 5.5 23538d640f refactor(tooling): T-1289 — economy-db becomes reach ledger
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>
2026-09-23 16:18:59 +02:00

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
]