"""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