feat(simulation): add corporate behavioral archetypes — Layer 3 (#809)

Implements the 6 behavioral archetypes from D-175 (Burnelli-Sheldon):

  Producer     — 1.15× production scale, neutral price signal
  Distributor  — 0.9× production, −3% price discount to move volume
  Specialist   — 1.0× production, +10% price premium for expertise
  Monopolist   — 0.8× production, withholds 25% of output, +20% premium
  Cooperative  — 1.0× production, −5% community discount
  Intermediary — 0.7× production, relies on traded goods

Archetype loading:
- Reads from corporations.behavioral_archetype (currently NULL for all corps)
- Falls back to heuristic inference from specialization text (freight → Distributor,
  extraction → Producer, luxury goods → Specialist, etc.)
- 48 corps loaded on current DB, all inferred (DB column to be populated when
  wiki corp frontmatter is extended with the behavioral_archetype field)

Applied in model.rs step():
- Per-corp effective_capacity = BASELINE_CAPACITY × production_scale
- Monopolist supply_withheld fraction reduces net output to stockpile
- Price premium: small ALPHA-scaled nudge to node price for primary commodity

All D-179 stability checks still pass with archetypes active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 22:27:00 +02:00
co-authored by Claude Sonnet 4.6
parent 26716bfc56
commit b19bfb3239
4 changed files with 224 additions and 10 deletions
+39 -10
View File
@@ -11,6 +11,7 @@
use std::collections::HashMap;
use crate::agents;
use crate::currency::{CurrencyState, ShadowEconomy};
use crate::db::Economy;
use crate::seed::Productivity;
@@ -93,12 +94,13 @@ pub fn run(
adjacency: &HashMap<String, Vec<String>>,
ticks: u32,
) -> Vec<TickRecord> {
let archetypes = agents::build_archetype_map(economy.corp_archetype_data.clone());
let mut nodes = init_nodes(economy);
let mut currency = CurrencyState::new();
let mut records = Vec::new();
for tick in 0..ticks {
step(economy, productivity, shadow, &mut nodes);
step(economy, productivity, shadow, &archetypes, &mut nodes);
trade::trade_step(economy, &mut nodes, adjacency, &mut currency);
currency.update_rate();
@@ -197,6 +199,7 @@ fn step(
economy: &Economy,
productivity: &HashMap<(String, String), Productivity>,
shadow: &ShadowEconomy,
archetypes: &HashMap<String, agents::Archetype>,
nodes: &mut HashMap<String, NodeState>,
) {
// Process each active node independently (Layer 1: no inter-system trade)
@@ -235,6 +238,15 @@ fn step(
None => continue,
};
// Layer 3: behavioral archetype parameters for this corporation
let arch_params = archetypes
.get(&corp_presence.corp_id)
.map(|a| a.params())
.unwrap_or_else(|| agents::Archetype::Producer.params());
// Effective baseline = BASELINE_CAPACITY scaled by archetype
let effective_capacity = BASELINE_CAPACITY * arch_params.production_scale;
// Determine the tier of the primary_operation commodity
let tier = economy
.commodity_map
@@ -244,10 +256,11 @@ fn step(
if tier == "raw" {
// Raw materials: direct extraction — no chain inputs required (D-177).
// Extraction rate multiplier applies.
let output = BASELINE_CAPACITY * prod.extraction_rate;
let gross_output = effective_capacity * prod.extraction_rate;
// Monopolist withholds a fraction of output
let net_output = gross_output * (1.0 - arch_params.supply_withheld);
if let Some(state) = node.commodities.get_mut(&primary_op) {
state.supply += output;
state.supply += net_output;
}
} else {
// Intermediate / final goods: run production chain with Leontief inputs.
@@ -262,7 +275,7 @@ fn step(
for input in &chain.inputs {
if let Some(state) = node.commodities.get(&input.commodity_id) {
let available = state.stockpile;
let required = input.quantity * BASELINE_CAPACITY;
let required = input.quantity * effective_capacity;
if required > 0.0 {
capacity_fraction = capacity_fraction
.min(available / required)
@@ -276,23 +289,39 @@ fn step(
// Apply productivity multiplier
let prod_mult = prod.for_tier(&chain_output_tier(economy, chain));
let actual_output =
BASELINE_CAPACITY * chain.output_quantity * capacity_fraction * prod_mult;
let gross_output =
effective_capacity * chain.output_quantity * capacity_fraction * prod_mult;
let net_output = gross_output * (1.0 - arch_params.supply_withheld);
// Consume inputs (Leontief: fixed-coefficient deduction)
for input in &chain.inputs {
if let Some(state) = node.commodities.get_mut(&input.commodity_id) {
let consumed = input.quantity * BASELINE_CAPACITY * capacity_fraction;
let consumed = input.quantity * effective_capacity * capacity_fraction;
state.stockpile = (state.stockpile - consumed).max(0.0);
}
}
// Add output to supply
// Add net output to supply
if let Some(state) = node.commodities.get_mut(&chain.output_commodity_id) {
state.supply += actual_output;
state.supply += net_output;
}
}
}
// Price premium: apply archetype price signal to primary commodity at this node.
// Positive premium pushes price up; negative discounts it.
// Applied as a small additive tâtonnement nudge capped to avoid instability.
if arch_params.price_premium.abs() > 1e-6 {
if let Some(state) = node.commodities.get_mut(&primary_op) {
let base_price = economy
.commodity_map
.get(&primary_op)
.map_or(1.0, |c| c.base_price);
let nudge = base_price * arch_params.price_premium * ALPHA;
state.price = (state.price + nudge)
.clamp(base_price * 0.05, base_price * 20.0);
}
}
}
// --- Demand step ---