Files
settled-reach/tooling/economy-db/economy_import/brands.py
T
jpmschweitzerandClaude Opus 5 338644b409 refactor(tooling): T-1286 — generate, pr and dev become reach domains
Twelve scripts retired, three domains registered. `reach` now covers nine.

generate: `generate-brands` and `generate-corporations` were the second and
third copies of the same 24-line build-if-missing-then-exec bash `tooling/atlas`
carried, so they collapsed into `core.process.cargo_binary` rather than being
ported. `import_economics` shelled out to the first of those, so it now calls
that helper — `generated_brands.toml` comes back byte-identical, and the stamp
registry swaps the retired wrapper for `core/process.py`.

pr: `watchlist-diff` derives its watched set from `generator_sources.py` instead
of restating it, so it cannot drift from the stamp check.

dev: the environment scripts split decision from performing, per D-263's
guarded-exec rule. `godot_plan()` and `worktree_plan()` decide what would
happen; `install_godot()`, `install_rust()` and `setup_worktree()` do it.
`tooling/test_environment.py` pins the version pin, both override precedences,
the already-current skip, the platform refusal and both worktree refusals —
none of them performed. `make setup` now installs reach first, since the
targets that install rust and godot are reach verbs.

Two live bugs found while porting:

- The clerk read its decision index from `decisions/README.md`, a path that
  stopped existing when the DQR tree moved to `governance/`. Every clerk agent
  has been grepping blind; its prompt pointed at the same dead directory.
- The conformance exec-check matched any `x.system()` regardless of receiver,
  so `platform.system()` read as `os.system()`. Narrowed and re-proved against
  a real mutant.

`process.run` gains `input=`, `timeout=` and a `ProcessTimeout` subclass so a
killed run stays distinguishable from a verdict. The pre-push hook no longer
merges the clerk's stderr into its stdout — under streaming the last merged
line is a JSONL event, which would read as an unrecognised verdict and block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:00:46 +02:00

255 lines
10 KiB
Python

"""Brand layer (D-189, #827): generate_brands shell-out, TOML import, validation."""
import sqlite3
import sys
import tomllib
from pathlib import Path
from .errors import ImportAborted
from .paths import BRANDS_TOML, GENERATED_BRANDS_TOML, REPO_ROOT
# economy-db/ is hyphenated, so it is not importable as a package and cannot
# reach `tooling.core` by normal import. The bootstrap goes away with T-1272
# (the hyphen sweep); until then it is explicit rather than implied.
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
VALID_BRAND_CATEGORIES: set[str] = {
"terroir", "heritage_craft", "tech_premium", "cultural",
"service_premium", "commodity_branded", "design_heritage", "platform_catalogue",
}
VALID_VALUE_TRAJECTORIES: set[str] = {"appreciating", "depreciating", "timeless"}
VALID_SCARCITY_CLASSES: set[str] = {"capped", "constrained", "scalable", "unlimited"}
VALID_BRAND_TIERS: set[str] = {"halo", "volume"}
VALID_CURRENCY_DENOMINATIONS: set[str] = {"tractus", "mark", "mixed", "sol_adjacent"}
VALID_PRICE_TIERS: set[str] = {"mass", "premium", "luxury", "flagship", "institutional"}
def regenerate_brands() -> None:
"""Run the Rust generate_brands binary to refresh generated_brands.toml.
Invoked as the first step of import_economics' main flow so the TOML on disk
always matches the current Rust source before the Python import reads it.
This replaces the former split (tooling/generate-brands run separately by
make regen-db) with a single, coherent brand pipeline owned by one stamp.
The binary is built on demand and run with the default canonical seed=1;
callers that need non-canonical seeds invoke it directly (experimentation
only — committed output must be seed=1).
This used to shell out to a `tooling/generate-brands` bash wrapper. The
wrapper was one of three identical copies of build-if-missing-then-exec, so
it was retired into `core.process.cargo_binary` (T-1286) and this calls that
helper instead. Same binary, same seed, same output.
"""
from tooling.core.errors import ReachError
from tooling.core.process import cargo_binary
print(" [pre/10] Running generate_brands (Rust) to refresh generated_brands.toml...")
try:
stdout = cargo_binary("generate_brands")
except ReachError as exc:
print(str(exc), file=sys.stderr)
raise ImportAborted() from exc
# Print the Rust binary's own summary lines (brands generated, coverage).
# Indent so they fold under the pre-step heading.
for line in stdout.splitlines():
if line.strip():
print(f" {line}")
def _load_brand_file(path: Path) -> tuple[list, list]:
"""Load brand_products and brand_inputs from a TOML file. Returns empty lists if missing."""
if not path.exists():
return [], []
with open(path, "rb") as f:
data = tomllib.load(f)
return data.get("brand_products", []), data.get("brand_inputs", [])
def import_brands(
conn: sqlite3.Connection, dry_run: bool
) -> tuple[int, int]:
"""Import brand_products and brand_inputs from brands.toml and generated_brands.toml.
Hand-authored brands (brands.toml) are imported first; generated brands
(generated_brands.toml, produced by `tooling/generate-brands`) are merged in.
Returns (n_products, n_inputs).
"""
if not BRANDS_TOML.exists():
print(" warning: brands.toml not found — brand layer skipped")
return 0, 0
products_authored, inputs_authored = _load_brand_file(BRANDS_TOML)
products_generated, inputs_generated = _load_brand_file(GENERATED_BRANDS_TOML)
if products_generated:
print(f" merging {len(products_generated)} generated brand_products from generated_brands.toml")
products = products_authored + products_generated
inputs = inputs_authored + inputs_generated
product_rows: list[tuple] = []
for p in products:
product_rows.append((
p["brand_product_id"],
p["corp_id"],
p["product_name"],
p["brand_category"],
p["value_trajectory"],
p["scarcity_class"],
p.get("product_subcategory"),
p.get("base_premium_multiplier", 1.0),
p.get("premium_floor", 0.0),
p.get("origin_system"),
int(p.get("terroir_locked", False)),
p.get("currency_denomination", "tractus"),
int(p.get("shadow_viable", False)),
p["brand_tier"],
p.get("halo_brand_id"),
p.get("price_tier"),
))
input_rows: list[tuple] = []
for inp in inputs:
input_rows.append((
inp["brand_product_id"],
inp["commodity_id"],
inp["quantity"],
))
if not dry_run:
conn.executemany(
"""INSERT OR REPLACE INTO brand_products (
brand_product_id, corp_id, product_name, brand_category,
value_trajectory, scarcity_class, product_subcategory,
base_premium_multiplier, premium_floor, origin_system,
terroir_locked, currency_denomination, shadow_viable,
brand_tier, halo_brand_id, price_tier
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
product_rows,
)
conn.executemany(
"""INSERT OR REPLACE INTO brand_inputs
(brand_product_id, commodity_id, quantity) VALUES (?, ?, ?)""",
input_rows,
)
return len(product_rows), len(input_rows)
def validate_brands(conn: sqlite3.Connection) -> list[str]:
"""Brand layer structural validation rules V-B01 through V-B06.
V-B01: Every brand_products row has a valid corp_id (FK to corporations).
V-B02: Every brand_inputs row has valid brand_product_id and commodity_id FKs.
V-B03: Every halo brand has at least one brand_inputs entry (demand stub must consume).
V-B04: Every volume tier must reference an existing halo brand_product_id.
V-B05: No brand_product_id is used as halo_brand_id by a non-volume-tier product.
V-B06: Every enum column (brand_category, value_trajectory, scarcity_class,
brand_tier, currency_denomination) is a member of its VALID_* set.
"""
errors: list[str] = []
# V-B01: brand_products → corporations FK
orphan_corps = conn.execute("""
SELECT bp.brand_product_id, bp.corp_id
FROM brand_products bp
LEFT JOIN corporations c ON bp.corp_id = c.corp_id
WHERE c.corp_id IS NULL
""").fetchall()
for pid, corp_id in orphan_corps:
errors.append(
f"V-B01: brand_product '{pid}' references unknown corp_id '{corp_id}'"
)
# V-B02: brand_inputs → brand_products and brand_inputs → commodities FKs
orphan_inputs_bp = conn.execute("""
SELECT bi.brand_product_id, bi.commodity_id
FROM brand_inputs bi
LEFT JOIN brand_products bp ON bi.brand_product_id = bp.brand_product_id
WHERE bp.brand_product_id IS NULL
""").fetchall()
for pid, cid in orphan_inputs_bp:
errors.append(
f"V-B02: brand_inputs row ({pid}, {cid}) references unknown brand_product_id"
)
orphan_inputs_comm = conn.execute("""
SELECT bi.brand_product_id, bi.commodity_id
FROM brand_inputs bi
LEFT JOIN commodities c ON bi.commodity_id = c.commodity_id
WHERE c.commodity_id IS NULL
""").fetchall()
for pid, cid in orphan_inputs_comm:
errors.append(
f"V-B02: brand_inputs row ({pid}, {cid}) references unknown commodity_id '{cid}'"
)
# V-B03: every halo brand has at least one brand_inputs entry
halo_no_inputs = conn.execute("""
SELECT bp.brand_product_id
FROM brand_products bp
WHERE bp.brand_tier = 'halo'
AND bp.brand_product_id NOT IN (SELECT brand_product_id FROM brand_inputs)
""").fetchall()
for (pid,) in halo_no_inputs:
errors.append(
f"V-B03: halo brand '{pid}' has no brand_inputs entries "
f"(must consume at least one commodity as a demand node)"
)
# V-B04: volume tiers reference valid halo_brand_id
volume_bad_halo = conn.execute("""
SELECT bp.brand_product_id, bp.halo_brand_id
FROM brand_products bp
WHERE bp.brand_tier = 'volume'
AND (bp.halo_brand_id IS NULL
OR bp.halo_brand_id NOT IN (SELECT brand_product_id FROM brand_products))
""").fetchall()
for pid, halo_id in volume_bad_halo:
errors.append(
f"V-B04: volume brand '{pid}' has invalid halo_brand_id '{halo_id}'"
)
# V-B05: halo_brand_id must only point to halo-tier products
halo_points_to_non_halo = conn.execute("""
SELECT child.brand_product_id, child.halo_brand_id, parent.brand_tier
FROM brand_products child
JOIN brand_products parent ON child.halo_brand_id = parent.brand_product_id
WHERE child.brand_tier = 'volume'
AND parent.brand_tier != 'halo'
""").fetchall()
for child_id, halo_id, parent_tier in halo_points_to_non_halo:
errors.append(
f"V-B05: volume brand '{child_id}' points to '{halo_id}' "
f"which has brand_tier='{parent_tier}', not 'halo'"
)
# V-B06: every enum column is in its VALID_* set. The SQL columns are
# plain TEXT without CHECK constraints, so a typo like `terrior` would
# otherwise silently import.
enum_checks: list[tuple[str, set[str]]] = [
("brand_category", VALID_BRAND_CATEGORIES),
("value_trajectory", VALID_VALUE_TRAJECTORIES),
("scarcity_class", VALID_SCARCITY_CLASSES),
("brand_tier", VALID_BRAND_TIERS),
("currency_denomination", VALID_CURRENCY_DENOMINATIONS),
("price_tier", VALID_PRICE_TIERS),
]
for column, valid_set in enum_checks:
bad = conn.execute(
f"SELECT brand_product_id, {column} FROM brand_products"
).fetchall()
for pid, value in bad:
if value is None:
continue # nullable columns (e.g. price_tier) may be unset
if value not in valid_set:
errors.append(
f"V-B06: brand_product '{pid}' has {column}='{value}' — "
f"must be one of {sorted(valid_set)}"
)
return errors