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>
255 lines
10 KiB
Python
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
|