From 03e0d1c02278a4235143d0eeaf59c005bcd6d8ef Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 14 Apr 2026 17:24:51 +0200 Subject: [PATCH] feat(simulation): brand layer schema and import pipeline (#827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the brand layer per D-189 §5: - Schema: brand_products, brand_inputs, system_fiscal, corp_financial_state, corp_lifecycle_events (+ 5 indexes). - Importer: reads wiki/economics/corporations/brands.toml, populates the new tables, validates V-B01–V-B05 structural rules, and derives system_fiscal for inhabited systems. - Data: 8 brand_products, 16 brand_inputs, 301 system_fiscal rows. Brand products are demand nodes — they consume commodities; they are not commodities themselves (D-185). Depends on copy PR #127 for the corp records referenced by brands.toml. --- server/data/systems-schema.sql | 60 +++++ server/data/systems.db | Bin 1118208 -> 1204224 bytes tooling/economy-db/import_economics.py | 294 +++++++++++++++++++++++- wiki/economics/corporations/brands.toml | 276 ++++++++++++++++++++++ 4 files changed, 620 insertions(+), 10 deletions(-) create mode 100644 wiki/economics/corporations/brands.toml diff --git a/server/data/systems-schema.sql b/server/data/systems-schema.sql index 4e45a8d3b..cca51818f 100644 --- a/server/data/systems-schema.sql +++ b/server/data/systems-schema.sql @@ -276,6 +276,60 @@ CREATE TABLE IF NOT EXISTS corp_presence ( PRIMARY KEY (corp_id, location_id) ); +-- Brand layer (D-189) — administered-price layer above commodity tâtonnement. +-- Brands consume commodities as demand nodes; they are NOT commodities (D-185). + +CREATE TABLE IF NOT EXISTS brand_products ( + brand_product_id TEXT PRIMARY KEY, + corp_id TEXT NOT NULL REFERENCES corporations(corp_id), + product_name TEXT NOT NULL, + brand_category TEXT NOT NULL, -- terroir|heritage_craft|tech_premium|cultural|service_premium|commodity_branded|design_heritage|platform_catalogue + value_trajectory TEXT NOT NULL, -- appreciating|depreciating|timeless + scarcity_class TEXT NOT NULL, -- capped|constrained|scalable|unlimited + product_subcategory TEXT, + base_premium_multiplier REAL NOT NULL DEFAULT 1.0, + premium_floor REAL NOT NULL DEFAULT 0.0, + origin_system TEXT REFERENCES star_systems(system_id), + terroir_locked INTEGER NOT NULL DEFAULT 0, -- boolean: production bound to origin_system + currency_denomination TEXT NOT NULL DEFAULT 'tractus', -- tractus|mark|mixed|sol_adjacent + shadow_viable INTEGER NOT NULL DEFAULT 0, -- boolean: circulates in shadow economy + brand_tier TEXT NOT NULL, -- halo|volume + halo_brand_id TEXT REFERENCES brand_products(brand_product_id), -- NULL for halo tier + updated_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS brand_inputs ( + brand_product_id TEXT NOT NULL REFERENCES brand_products(brand_product_id), + commodity_id TEXT NOT NULL REFERENCES commodities(commodity_id), + quantity REAL NOT NULL, + PRIMARY KEY (brand_product_id, commodity_id) +); + +-- Fiscal parameters per star system (D-189 section 5 + section 6) +CREATE TABLE IF NOT EXISTS system_fiscal ( + system_id TEXT PRIMARY KEY REFERENCES star_systems(system_id), + corp_tax_rate REAL NOT NULL DEFAULT 0.22, -- 0.0–1.0 + collection_efficiency REAL NOT NULL DEFAULT 1.0, -- derived: 1.0 - shadow_intensity × 0.6 + updated_at TEXT DEFAULT (datetime('now')) +); + +-- Phase 2: passive corp health tracking (D-189 section 8) +CREATE TABLE IF NOT EXISTS corp_financial_state ( + corp_id TEXT PRIMARY KEY REFERENCES corporations(corp_id), + health_metric REAL NOT NULL DEFAULT 1.0, -- 0.0 (distressed) to 1.0 (healthy) + updated_at TEXT DEFAULT (datetime('now')) +); + +-- Phase 3 lifecycle state machine stub (D-189 section 8) — schema correct, not driven yet +CREATE TABLE IF NOT EXISTS corp_lifecycle_events ( + event_id INTEGER PRIMARY KEY AUTOINCREMENT, + corp_id TEXT NOT NULL REFERENCES corporations(corp_id), + event_type TEXT NOT NULL, -- Founded|Growing|Active|Distressed|Acquired|Dissolved + event_tick INTEGER NOT NULL DEFAULT 0, + event_data TEXT, -- JSON blob for future lifecycle detail + created_at TEXT DEFAULT (datetime('now')) +); + -- Indexes -- astronomical_id removed: system_id IS the GJ catalog number CREATE INDEX IF NOT EXISTS idx_star_systems_sector ON star_systems(geographic_sector); @@ -304,3 +358,9 @@ CREATE INDEX IF NOT EXISTS idx_production_chains_output ON production_chains(out CREATE INDEX IF NOT EXISTS idx_chain_inputs_commodity ON chain_inputs(input_commodity_id); CREATE INDEX IF NOT EXISTS idx_corp_presence_corp ON corp_presence(corp_id); CREATE INDEX IF NOT EXISTS idx_corp_presence_location ON corp_presence(location_id); +-- Brand layer indexes (D-189 section 5 — composite for UI queries) +CREATE INDEX IF NOT EXISTS idx_brand_products_corp_category ON brand_products(corp_id, brand_category); +CREATE INDEX IF NOT EXISTS idx_brand_products_origin ON brand_products(origin_system); +CREATE INDEX IF NOT EXISTS idx_brand_products_tier ON brand_products(brand_tier); +CREATE INDEX IF NOT EXISTS idx_brand_inputs_commodity ON brand_inputs(commodity_id); +CREATE INDEX IF NOT EXISTS idx_corp_lifecycle_events_corp ON corp_lifecycle_events(corp_id); diff --git a/server/data/systems.db b/server/data/systems.db index 178080bf0f75bb6527c104ce1f7fd42444a9386e..95151e106cd5f58e04cfbb18bc82c25b92939941 100644 GIT binary patch delta 29867 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 delta 4299 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 diff --git a/tooling/economy-db/import_economics.py b/tooling/economy-db/import_economics.py index 5b318a707..2ef3d697c 100755 --- a/tooling/economy-db/import_economics.py +++ b/tooling/economy-db/import_economics.py @@ -39,6 +39,7 @@ COMMODITIES_TOML = REPO_ROOT / "wiki" / "economics" / "commodities.toml" CHAINS_TOML = REPO_ROOT / "wiki" / "economics" / "production_chains.toml" SCHEMA_SQL = REPO_ROOT / "server" / "data" / "systems-schema.sql" CORPORATIONS_DIR = REPO_ROOT / "wiki" / "corporations" +BRANDS_TOML = REPO_ROOT / "wiki" / "economics" / "corporations" / "brands.toml" # --------------------------------------------------------------------------- @@ -48,6 +49,61 @@ CORPORATIONS_DIR = REPO_ROOT / "wiki" / "corporations" MIGRATION_SQL = """ -- Economics tables (idempotent — safe to re-run) +-- Brand layer tables (D-189, #827) +CREATE TABLE IF NOT EXISTS brand_products ( + brand_product_id TEXT PRIMARY KEY, + corp_id TEXT NOT NULL REFERENCES corporations(corp_id), + product_name TEXT NOT NULL, + brand_category TEXT NOT NULL, + value_trajectory TEXT NOT NULL, + scarcity_class TEXT NOT NULL, + product_subcategory TEXT, + base_premium_multiplier REAL NOT NULL DEFAULT 1.0, + premium_floor REAL NOT NULL DEFAULT 0.0, + origin_system TEXT REFERENCES star_systems(system_id), + terroir_locked INTEGER NOT NULL DEFAULT 0, + currency_denomination TEXT NOT NULL DEFAULT 'tractus', + shadow_viable INTEGER NOT NULL DEFAULT 0, + brand_tier TEXT NOT NULL, + halo_brand_id TEXT REFERENCES brand_products(brand_product_id), + updated_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS brand_inputs ( + brand_product_id TEXT NOT NULL REFERENCES brand_products(brand_product_id), + commodity_id TEXT NOT NULL REFERENCES commodities(commodity_id), + quantity REAL NOT NULL, + PRIMARY KEY (brand_product_id, commodity_id) +); + +CREATE TABLE IF NOT EXISTS system_fiscal ( + system_id TEXT PRIMARY KEY REFERENCES star_systems(system_id), + corp_tax_rate REAL NOT NULL DEFAULT 0.22, + collection_efficiency REAL NOT NULL DEFAULT 1.0, + updated_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS corp_financial_state ( + corp_id TEXT PRIMARY KEY REFERENCES corporations(corp_id), + health_metric REAL NOT NULL DEFAULT 1.0, + updated_at TEXT DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS corp_lifecycle_events ( + event_id INTEGER PRIMARY KEY AUTOINCREMENT, + corp_id TEXT NOT NULL REFERENCES corporations(corp_id), + event_type TEXT NOT NULL, + event_tick INTEGER NOT NULL DEFAULT 0, + event_data TEXT, + created_at TEXT DEFAULT (datetime('now')) +); + +CREATE INDEX IF NOT EXISTS idx_brand_products_corp_category ON brand_products(corp_id, brand_category); +CREATE INDEX IF NOT EXISTS idx_brand_products_origin ON brand_products(origin_system); +CREATE INDEX IF NOT EXISTS idx_brand_products_tier ON brand_products(brand_tier); +CREATE INDEX IF NOT EXISTS idx_brand_inputs_commodity ON brand_inputs(commodity_id); +CREATE INDEX IF NOT EXISTS idx_corp_lifecycle_events_corp ON corp_lifecycle_events(corp_id); + CREATE TABLE IF NOT EXISTS gate_links ( from_system_id TEXT NOT NULL REFERENCES star_systems(system_id), to_system_id TEXT NOT NULL REFERENCES star_systems(system_id), @@ -669,6 +725,206 @@ def _validate_system_coverage( ] +# --------------------------------------------------------------------------- +# Brand layer import (D-189, #827) +# --------------------------------------------------------------------------- + +VALID_BRAND_CATEGORIES = { + "terroir", "heritage_craft", "tech_premium", "cultural", + "service_premium", "commodity_branded", "design_heritage", "platform_catalogue", +} +VALID_VALUE_TRAJECTORIES = {"appreciating", "depreciating", "timeless"} +VALID_SCARCITY_CLASSES = {"capped", "constrained", "scalable", "unlimited"} +VALID_BRAND_TIERS = {"halo", "volume"} +VALID_CURRENCY_DENOMINATIONS = {"tractus", "mark", "mixed", "sol_adjacent"} + + +def import_brands( + conn: sqlite3.Connection, dry_run: bool +) -> tuple[int, int]: + """Import brand_products and brand_inputs from wiki/economics/corporations/brands.toml. + + Returns (n_products, n_inputs). + """ + if not BRANDS_TOML.exists(): + print(" warning: brands.toml not found — brand layer skipped") + return 0, 0 + + with open(BRANDS_TOML, "rb") as f: + data = tomllib.load(f) + + products = data.get("brand_products", []) + inputs = data.get("brand_inputs", []) + + product_rows = [] + 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"), + )) + + input_rows = [] + 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 + ) 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 import_system_fiscal(conn: sqlite3.Connection, dry_run: bool) -> int: + """Populate system_fiscal from star_systems shadow_economy data. + + collection_efficiency = 1.0 - shadow_economy_intensity × 0.6 (D-189 section 6). + shadow_economy_intensity comes from the shadow_economy.toml pipeline or defaults to 0. + Uses corp_tax_rate = 0.22 (default) for all systems in Phase 2. + """ + inhabited = conn.execute(""" + SELECT ss.system_id, COALESCE(se.population, 0) + FROM star_systems ss + LEFT JOIN system_economy se ON ss.system_id = se.system_id + WHERE ss.inhabited_planet_count > 0 OR se.population > 0 + ORDER BY ss.system_id + """).fetchall() + + rows = [] + for system_id, _pop in inhabited: + # Phase 2: shadow_economy_intensity not yet per-system in DB. + # Default collection_efficiency to 0.85 (mid-reach average) until + # shadow_economy.toml pipeline is wired (Phase 3). + rows.append((system_id, 0.22, 0.85)) + + if not dry_run: + conn.executemany( + """INSERT OR IGNORE INTO system_fiscal + (system_id, corp_tax_rate, collection_efficiency) VALUES (?, ?, ?)""", + rows, + ) + + return len(rows) + + +def validate_brands(conn: sqlite3.Connection) -> list[str]: + """Brand layer structural validation rules V-B01 through V-B05. + + 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. + """ + 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'" + ) + + return errors + + # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- @@ -699,7 +955,7 @@ def main(): conn.execute("PRAGMA foreign_keys=ON") # 1. Migrate schema - print(" [1/8] Schema migration...") + print(" [1/10] Schema migration...") for table, col, col_type in COLUMN_MIGRATIONS: _add_column(conn, table, col, col_type) conn.executescript(MIGRATION_SQL) @@ -709,40 +965,45 @@ def main(): # corp_presence cleared here; corporations table is append-only (never cleared) if not args.dry_run: conn.execute("DELETE FROM corp_presence") + conn.execute("DELETE FROM brand_inputs") + conn.execute("DELETE FROM brand_products") + conn.execute("DELETE FROM system_fiscal") + conn.execute("DELETE FROM corp_financial_state") + conn.execute("DELETE FROM corp_lifecycle_events") conn.execute("DELETE FROM chain_inputs") conn.execute("DELETE FROM production_chains") conn.execute("DELETE FROM commodities") conn.execute("DELETE FROM gate_links") # 2. Gate links - print(" [2/8] Importing gate links...") + print(" [2/10] Importing gate links...") n_links = import_gate_links(conn, args.dry_run) print(f" {n_links} rows (bidirectional)") # 3. Commodities - print(" [3/8] Importing commodities...") + print(" [3/10] Importing commodities...") n_commodities = import_commodities(conn, args.dry_run) print(f" {n_commodities} commodities") # 4. Production chains - print(" [4/8] Importing production chains...") + print(" [4/10] Importing production chains...") n_chains, n_inputs = import_chains(conn, args.dry_run) print(f" {n_chains} chains, {n_inputs} inputs") # 5. Currency zones - print(" [5/8] Setting currency zones...") + print(" [5/10] Setting currency zones...") zones = set_currency_zones(conn, args.dry_run) for zone, count in sorted(zones.items()): print(f" {zone}: {count}") # 6. Gate energy connectivity (D-186) — must run after currency zones - print(" [6/8] Setting gate energy connectivity...") + print(" [6/10] Setting gate energy connectivity...") energy = set_gate_energy(conn, args.dry_run) for label, count in sorted(energy.items()): print(f" {label}: {count}") # 7. Sync corporations from wiki (D-182: hard error on name divergence) - print(" [7/8] Syncing corporations...") + print(" [7/10] Syncing corporations...") corp_errors = sync_corporations(conn, wiki_corps, args.dry_run) if corp_errors: print(f" ERRORS ({len(corp_errors)}) — name divergence detected (D-182):") @@ -755,17 +1016,28 @@ def main(): print(f" {n_db_corps} corporations in DB ({len(wiki_corps)} from wiki)") # 8. Corp presence from wiki headquarters data - print(" [8/8] Importing corp presence...") + print(" [8/10] Importing corp presence...") commodity_ids = { r[0] for r in conn.execute("SELECT commodity_id FROM commodities").fetchall() } n_presence = import_corp_presence(conn, wiki_corps, commodity_ids, args.dry_run) print(f" {n_presence} corp_presence rows") + # 9. Brand products and inputs (D-189, #827) + print(" [9/10] Importing brand products and inputs...") + n_brands, n_brand_inputs = import_brands(conn, args.dry_run) + print(f" {n_brands} brand_products, {n_brand_inputs} brand_inputs") + + # 10. System fiscal parameters (D-189 section 6) + print(" [10/10] Populating system_fiscal...") + n_fiscal = import_system_fiscal(conn, args.dry_run) + print(f" {n_fiscal} system_fiscal rows") + # Validate structural integrity (FK, chain refs, chain completeness). # These errors indicate broken imported data — do NOT commit. print("\n Validating structural integrity...") struct_errors = validate(conn) + struct_errors.extend(validate_brands(conn)) if struct_errors: print(f" STRUCTURAL ERRORS ({len(struct_errors)}) — rolling back:") for e in struct_errors: @@ -773,7 +1045,7 @@ def main(): conn.close() sys.exit(1) else: - print(" FK integrity and chain completeness OK") + print(" FK integrity, chain completeness, and brand layer (V-B01–V-B05) OK") # Commit all imported data (corps, presence, etc.) before coverage check. # Coverage validation is a Phase 2 gate (D-175) — data should be persisted @@ -809,7 +1081,9 @@ def main(): conn.close() print(f"\n Done: {n_links} gate_links, {n_commodities} commodities, " - f"{n_chains} chains, {n_inputs} inputs, {n_presence} corp_presence\n") + f"{n_chains} chains, {n_inputs} inputs, {n_presence} corp_presence, " + f"{n_brands} brand_products, {n_brand_inputs} brand_inputs, " + f"{n_fiscal} system_fiscal\n") if __name__ == "__main__": diff --git a/wiki/economics/corporations/brands.toml b/wiki/economics/corporations/brands.toml new file mode 100644 index 000000000..40034a74e --- /dev/null +++ b/wiki/economics/corporations/brands.toml @@ -0,0 +1,276 @@ +# ========================================================================== +# Settled Reach — Brand Product Records (Phase 2 Demand Stubs) +# Source of truth for brand_products and brand_inputs tables. +# Compiled to systems.db via `make economy-db`. +# +# These 4 canonical brand corps register as commodity demand nodes in the +# tâtonnement simulation (D-185: brands consume commodities, not the reverse). +# Pricing model and cultural premium curves are Phase 3+ deliverables (D-189). +# +# Fields per [[brand_products]] entry: +# brand_product_id Unique slug (corp_id + product slug) +# corp_id Must match a slug in wiki/corporations/ +# product_name Display name +# brand_category terroir|heritage_craft|tech_premium|cultural| +# service_premium|commodity_branded|design_heritage|platform_catalogue +# value_trajectory appreciating|depreciating|timeless +# scarcity_class capped|constrained|scalable|unlimited +# product_subcategory Readable product type descriptor +# base_premium_multiplier Multiplier over raw commodity input cost (Phase 3 pricing) +# premium_floor Minimum administered price floor (Phase 3 pricing) +# origin_system GJ catalog ID — must match star_systems.system_id +# terroir_locked true: production cannot move from origin_system +# currency_denomination tractus|mark|mixed|sol_adjacent +# shadow_viable true: circulates in shadow economy +# brand_tier halo|volume +# halo_brand_id For volume tiers: brand_product_id of parent halo product +# +# Fields per [[brand_inputs]] entry: +# brand_product_id References a brand_products entry above +# commodity_id Must match commodities.toml +# quantity Demand coefficient (annual units consumed per production run) +# +# Decisions: D-185 (brands as demand nodes), D-189 (brand layer architecture), +# D-190 (volume calibration), D-182 (TOML pipeline) +# ========================================================================== + + +# ========================================================================== +# CALLOWAY DISTILLERY (north_reach — terroir, appreciating) +# Eleven distilleries, 400 years of production. GJ 3325. +# Inputs: grain (agricultural_produce) + water for single malt whisky. +# ========================================================================== + +[[brand_products]] +brand_product_id = "calloway-single-malt-halo" +corp_id = "calloway-distillery" +product_name = "Calloway Single Malt 25yr" +brand_category = "terroir" +value_trajectory = "appreciating" +scarcity_class = "capped" +product_subcategory = "aged_spirits" +base_premium_multiplier = 18.0 +premium_floor = 0.85 +origin_system = "GJ 3325" +terroir_locked = true +currency_denomination = "tractus" +shadow_viable = true +brand_tier = "halo" + +[[brand_inputs]] +brand_product_id = "calloway-single-malt-halo" +commodity_id = "agricultural_produce" +quantity = 0.12 # grain — ~12% of annual produce demand per distillery output unit + +[[brand_inputs]] +brand_product_id = "calloway-single-malt-halo" +commodity_id = "water" +quantity = 0.08 # distillation water + +[[brand_products]] +brand_product_id = "calloway-reserve-volume" +corp_id = "calloway-distillery" +product_name = "Calloway Reserve" +brand_category = "terroir" +value_trajectory = "appreciating" +scarcity_class = "constrained" +product_subcategory = "aged_spirits" +base_premium_multiplier = 4.5 +premium_floor = 0.30 +origin_system = "GJ 3325" +terroir_locked = true +currency_denomination = "tractus" +shadow_viable = true +brand_tier = "volume" +halo_brand_id = "calloway-single-malt-halo" + +[[brand_inputs]] +brand_product_id = "calloway-reserve-volume" +commodity_id = "agricultural_produce" +quantity = 0.55 # larger grain demand — volume tier drives commodity draw + +[[brand_inputs]] +brand_product_id = "calloway-reserve-volume" +commodity_id = "water" +quantity = 0.35 + + +# ========================================================================== +# VINS DE GRAND VIDE (west_reach — terroir, appreciating / timeless) +# Négociant cooperative, GJ 395. Wine production from châteaux network. +# Inputs: agricultural_produce (grapes) + water. +# ========================================================================== + +[[brand_products]] +brand_product_id = "vgv-premier-cru-halo" +corp_id = "vins-de-grand-vide" +product_name = "Grand Vide Premier Cru" +brand_category = "terroir" +value_trajectory = "appreciating" +scarcity_class = "capped" +product_subcategory = "fine_wine" +base_premium_multiplier = 12.0 +premium_floor = 0.70 +origin_system = "GJ 395" +terroir_locked = true +currency_denomination = "tractus" +shadow_viable = true +brand_tier = "halo" + +[[brand_inputs]] +brand_product_id = "vgv-premier-cru-halo" +commodity_id = "agricultural_produce" +quantity = 0.18 # estate grapes — limited châteaux harvest + +[[brand_inputs]] +brand_product_id = "vgv-premier-cru-halo" +commodity_id = "water" +quantity = 0.04 + +[[brand_products]] +brand_product_id = "vgv-standard-volume" +corp_id = "vins-de-grand-vide" +product_name = "Grand Vide Standard" +brand_category = "terroir" +value_trajectory = "timeless" +scarcity_class = "scalable" +product_subcategory = "wine" +base_premium_multiplier = 2.2 +premium_floor = 0.15 +origin_system = "GJ 395" +terroir_locked = false +currency_denomination = "tractus" +shadow_viable = false +brand_tier = "volume" +halo_brand_id = "vgv-premier-cru-halo" + +[[brand_inputs]] +brand_product_id = "vgv-standard-volume" +commodity_id = "agricultural_produce" +quantity = 0.80 # négociant aggregation — largest agricultural demand node + +[[brand_inputs]] +brand_product_id = "vgv-standard-volume" +commodity_id = "water" +quantity = 0.20 + + +# ========================================================================== +# THRDS (north_reach — heritage_craft, timeless) +# Cold-weather technical clothing cooperative, GJ 475. +# Inputs: textiles (brach fiber) + organic_compounds (dye, finish). +# ========================================================================== + +[[brand_products]] +brand_product_id = "thrds-origin-halo" +corp_id = "thrds" +product_name = "thrds Origin" +brand_category = "heritage_craft" +value_trajectory = "timeless" +scarcity_class = "constrained" +product_subcategory = "technical_clothing" +base_premium_multiplier = 6.0 +premium_floor = 0.50 +origin_system = "GJ 475" +terroir_locked = true +currency_denomination = "tractus" +shadow_viable = false +brand_tier = "halo" + +[[brand_inputs]] +brand_product_id = "thrds-origin-halo" +commodity_id = "textiles" +quantity = 0.30 # brach fiber — origin-specific weave + +[[brand_inputs]] +brand_product_id = "thrds-origin-halo" +commodity_id = "organic_compounds" +quantity = 0.08 # plant-derived dyes and finishing compounds + +[[brand_products]] +brand_product_id = "thrds-standard-volume" +corp_id = "thrds" +product_name = "thrds Standard" +brand_category = "heritage_craft" +value_trajectory = "timeless" +scarcity_class = "scalable" +product_subcategory = "technical_clothing" +base_premium_multiplier = 2.8 +premium_floor = 0.20 +origin_system = "GJ 475" +terroir_locked = false +currency_denomination = "tractus" +shadow_viable = false +brand_tier = "volume" +halo_brand_id = "thrds-origin-halo" + +[[brand_inputs]] +brand_product_id = "thrds-standard-volume" +commodity_id = "textiles" +quantity = 1.20 # primary textile demand driver (volume tier production) + +[[brand_inputs]] +brand_product_id = "thrds-standard-volume" +commodity_id = "organic_compounds" +quantity = 0.25 + + +# ========================================================================== +# BÍFRÖST MARMOR (Compact / north_reach — terroir, appreciating) +# Kvitfjell moon marble quarry, Nyrheim (GJ 3737). Nyrheim Cooperative subsidiary. +# Inputs: stone (raw marble) + chemicals (polishing, finishing agents). +# Shadow viable: Compact-adjacent, some Sol trade on interior luxury markets. +# ========================================================================== + +[[brand_products]] +brand_product_id = "bifrost-grade-a-halo" +corp_id = "bifrost-marmor" +product_name = "Kvitfjell Grade A" +brand_category = "terroir" +value_trajectory = "appreciating" +scarcity_class = "capped" +product_subcategory = "architectural_stone" +base_premium_multiplier = 22.0 +premium_floor = 1.20 +origin_system = "GJ 3737" +terroir_locked = true +currency_denomination = "mark" +shadow_viable = true +brand_tier = "halo" + +[[brand_inputs]] +brand_product_id = "bifrost-grade-a-halo" +commodity_id = "stone" +quantity = 0.40 # high-purity calcite marble extraction — geological scarcity + +[[brand_inputs]] +brand_product_id = "bifrost-grade-a-halo" +commodity_id = "chemicals" +quantity = 0.10 # precision polishing compounds + +[[brand_products]] +brand_product_id = "bifrost-commercial-volume" +corp_id = "bifrost-marmor" +product_name = "Kvitfjell Commercial" +brand_category = "terroir" +value_trajectory = "appreciating" +scarcity_class = "constrained" +product_subcategory = "architectural_stone" +base_premium_multiplier = 5.5 +premium_floor = 0.40 +origin_system = "GJ 3737" +terroir_locked = true +currency_denomination = "mixed" +shadow_viable = true +brand_tier = "volume" +halo_brand_id = "bifrost-grade-a-halo" + +[[brand_inputs]] +brand_product_id = "bifrost-commercial-volume" +commodity_id = "stone" +quantity = 1.50 # commercial-grade quarrying — volume demand node + +[[brand_inputs]] +brand_product_id = "bifrost-commercial-volume" +commodity_id = "chemicals" +quantity = 0.30