Files
settled-reach/tooling/econ-sim/src/db.rs
T
jpmschweitzerandClaude Opus 4.6 d45cfe0fa3 fix(simulation): address PR #122 review — determinism, correctness, labeling
- HashMap → BTreeMap throughout econ-sim for deterministic iteration (D-010)
- Fix cost_factor: multiplicative gate×zone instead of additive (trade.rs)
- Extract derive_seed to shared prng.rs, consolidate FNV-1a implementation
- Rename run_shock_test → run_no_explosion_check (not D-179 Test 3)
- Deduplicate cross-zone FX rate collection in Test 4
- Replace ORDER BY RANDOM() with deterministic ordering + ChaCha8Rng
- Make commodity coverage failure a hard error consistent with D-175
- Fix gap-fill off-by-one (4 corps → 3 when coverage = 0)
- Correct test report: EconEvent exists, location_type is body/station

All four D-179 stability tests still pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:51:23 +02:00

358 lines
11 KiB
Rust

//! Database loading — reads economy data from systems.db.
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process;
use rusqlite::Connection;
// ---------------------------------------------------------------------------
// Data types
// ---------------------------------------------------------------------------
#[derive(Debug, Clone)]
pub struct Commodity {
pub id: String,
// Display name — used in reporting (#807+):
#[allow(dead_code)]
pub name: String,
pub tier: String,
pub base_price: f64,
// Used by Layer 2+ pricing (#807, #808):
#[allow(dead_code)]
pub elasticity: String,
#[allow(dead_code)]
pub production_ubiquity: Option<String>,
#[allow(dead_code)]
pub demand_model: String,
}
#[derive(Debug, Clone)]
pub struct ChainInput {
pub commodity_id: String,
pub quantity: f64,
}
#[derive(Debug, Clone)]
pub struct ProductionChain {
pub chain_id: String,
pub output_commodity_id: String,
pub output_quantity: f64,
// Used by Layer 2+ for location-constrained production (#807):
#[allow(dead_code)]
pub location_bound: bool,
pub inputs: Vec<ChainInput>,
}
#[derive(Debug, Clone)]
pub struct CorpPresence {
pub corp_id: String,
pub system_id: String,
pub primary_operation: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SystemInfo {
pub system_id: String,
// Used for display/reporting in #807+:
#[allow(dead_code)]
pub proper_name: Option<String>,
pub population: i64,
pub cultural_corridor: Option<String>,
pub gate_energy_connected: bool,
/// Currency zone: TRACTUS_PRIMARY | MARK_PRIMARY | MIXED (D-171, D-172)
pub currency_zone: String,
/// Hop count from the nearest gateway — used for shadow economy seeding (D-174)
pub hop_distance: i64,
/// Gate topology type — used for shadow economy seeding (D-174)
pub gate_topology: Option<String>,
/// Political zone — used for shadow economy seeding (D-174)
pub political_zone: Option<String>,
}
/// A directed gate link between two systems.
#[derive(Debug, Clone)]
pub struct GateLink {
pub from_system_id: String,
pub to_system_id: String,
}
/// The complete economics dataset loaded from systems.db.
pub struct Economy {
pub commodities: Vec<Commodity>,
pub commodity_map: BTreeMap<String, Commodity>,
pub chains: Vec<ProductionChain>,
/// Map: output_commodity_id → list of chains that produce it
pub chains_by_output: BTreeMap<String, Vec<ProductionChain>>,
/// Map: system_id → SystemInfo
pub systems: BTreeMap<String, SystemInfo>,
pub corp_presences: Vec<CorpPresence>,
/// Map: system_id → list of corp presences
pub presences_by_system: BTreeMap<String, Vec<CorpPresence>>,
/// Bidirectional gate links (transport graph)
pub gate_links: Vec<GateLink>,
/// Raw corp data for archetype inference: (corp_id, behavioral_archetype?, specialization?)
pub corp_archetype_data: Vec<(String, Option<String>, Option<String>)>,
}
// ---------------------------------------------------------------------------
// DB helpers
// ---------------------------------------------------------------------------
pub fn resolve_db_path(explicit: Option<PathBuf>) -> PathBuf {
if let Some(p) = explicit {
return p;
}
let mut dir = std::env::current_dir().expect("Cannot determine CWD");
loop {
let candidate = dir.join("server").join("data").join("systems.db");
if candidate.exists() {
return candidate;
}
if !dir.pop() {
break;
}
}
eprintln!("error: cannot find server/data/systems.db — pass --db explicitly");
process::exit(1);
}
pub fn open_db(path: &PathBuf) -> Connection {
let conn = Connection::open(path).unwrap_or_else(|e| {
eprintln!("error: cannot open {}: {}", path.display(), e);
process::exit(1);
});
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
.expect("PRAGMA setup failed");
conn
}
// ---------------------------------------------------------------------------
// Loaders
// ---------------------------------------------------------------------------
fn load_commodities(conn: &Connection) -> Vec<Commodity> {
let mut stmt = conn
.prepare(
"SELECT commodity_id, name, tier, base_price, elasticity,
production_ubiquity, demand_model
FROM commodities ORDER BY commodity_id",
)
.expect("prepare commodities");
stmt.query_map([], |row| {
Ok(Commodity {
id: row.get(0)?,
name: row.get(1)?,
tier: row.get(2)?,
base_price: row.get(3)?,
elasticity: row.get(4)?,
production_ubiquity: row.get(5)?,
demand_model: row.get::<_, Option<String>>(6)?.unwrap_or_default(),
})
})
.expect("query commodities")
.filter_map(|r| r.ok())
.collect()
}
fn load_chains(conn: &Connection) -> Vec<ProductionChain> {
let mut chain_stmt = conn
.prepare(
"SELECT chain_id, output_commodity_id, output_quantity, location_bound
FROM production_chains ORDER BY chain_id",
)
.expect("prepare chains");
let mut chains: Vec<ProductionChain> = chain_stmt
.query_map([], |row| {
Ok(ProductionChain {
chain_id: row.get(0)?,
output_commodity_id: row.get(1)?,
output_quantity: row.get(2)?,
location_bound: row.get::<_, i32>(3)? != 0,
inputs: Vec::new(),
})
})
.expect("query chains")
.filter_map(|r| r.ok())
.collect();
// Load inputs for each chain
let mut input_stmt = conn
.prepare(
"SELECT chain_id, input_commodity_id, quantity
FROM chain_inputs ORDER BY chain_id, input_commodity_id",
)
.expect("prepare chain_inputs");
let all_inputs: Vec<(String, String, f64)> = input_stmt
.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
.expect("query chain_inputs")
.filter_map(|r| r.ok())
.collect();
// Build index of chain_id → inputs
let mut input_map: BTreeMap<String, Vec<ChainInput>> = BTreeMap::new();
for (chain_id, commodity_id, quantity) in all_inputs {
input_map.entry(chain_id).or_default().push(ChainInput {
commodity_id,
quantity,
});
}
for chain in &mut chains {
if let Some(inputs) = input_map.remove(&chain.chain_id) {
chain.inputs = inputs;
}
}
chains
}
fn load_systems(conn: &Connection) -> BTreeMap<String, SystemInfo> {
let mut stmt = conn
.prepare(
"SELECT ss.system_id, ss.proper_name, ss.cultural_corridor,
ss.gate_energy_connected,
COALESCE(se.population, 0) as population,
COALESCE(ss.currency_zone, 'TRACTUS_PRIMARY') as currency_zone,
COALESCE(sg.hop_distance_from_gateway, 5) as hop_distance,
sg.gate_topology,
ss.political_zone
FROM star_systems ss
LEFT JOIN system_economy se ON ss.system_id = se.system_id
LEFT JOIN system_gates sg ON ss.system_id = sg.system_id
ORDER BY ss.system_id",
)
.expect("prepare systems");
stmt.query_map([], |row| {
Ok(SystemInfo {
system_id: row.get(0)?,
proper_name: row.get(1)?,
cultural_corridor: row.get(2)?,
gate_energy_connected: row.get::<_, Option<i32>>(3)?.unwrap_or(1) != 0,
population: row.get(4)?,
currency_zone: row
.get::<_, Option<String>>(5)?
.unwrap_or_else(|| "TRACTUS_PRIMARY".to_string()),
hop_distance: row.get::<_, Option<i64>>(6)?.unwrap_or(5),
gate_topology: row.get(7)?,
political_zone: row.get(8)?,
})
})
.expect("query systems")
.filter_map(|r| r.ok())
.map(|s| (s.system_id.clone(), s))
.collect()
}
fn load_corp_archetype_data(conn: &Connection) -> Vec<(String, Option<String>, Option<String>)> {
let mut stmt = conn
.prepare(
"SELECT corp_id, behavioral_archetype, specialization
FROM corporations ORDER BY corp_id",
)
.expect("prepare corp archetype data");
stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
.expect("query corp archetype data")
.filter_map(|r| r.ok())
.collect()
}
fn load_gate_links(conn: &Connection) -> Vec<GateLink> {
let mut stmt = conn
.prepare(
"SELECT from_system_id, to_system_id FROM gate_links
ORDER BY from_system_id, to_system_id",
)
.expect("prepare gate_links");
stmt.query_map([], |row| {
Ok(GateLink {
from_system_id: row.get(0)?,
to_system_id: row.get(1)?,
})
})
.expect("query gate_links")
.filter_map(|r| r.ok())
.collect()
}
fn load_corp_presences(conn: &Connection) -> Vec<CorpPresence> {
// Resolve body/station location_id back to system_id via LEFT JOINs.
// corp_presence.location_type is 'body' | 'station' per schema.
let mut stmt = conn
.prepare(
"SELECT cp.corp_id,
COALESCE(b.system_id, s.system_id) AS system_id,
cp.primary_operation
FROM corp_presence cp
LEFT JOIN bodies b ON cp.location_type = 'body' AND cp.location_id = b.body_id
LEFT JOIN stations s ON cp.location_type = 'station' AND cp.location_id = s.station_id
WHERE COALESCE(b.system_id, s.system_id) IS NOT NULL
ORDER BY system_id, cp.corp_id",
)
.expect("prepare corp_presence");
stmt.query_map([], |row| {
Ok(CorpPresence {
corp_id: row.get(0)?,
system_id: row.get(1)?,
primary_operation: row.get(2)?,
})
})
.expect("query corp_presence")
.filter_map(|r| r.ok())
.collect()
}
// ---------------------------------------------------------------------------
// Main loader
// ---------------------------------------------------------------------------
pub fn load_economy(conn: &Connection) -> Economy {
let commodities = load_commodities(conn);
let commodity_map: BTreeMap<String, Commodity> = commodities
.iter()
.map(|c| (c.id.clone(), c.clone()))
.collect();
let chains = load_chains(conn);
let mut chains_by_output: BTreeMap<String, Vec<ProductionChain>> = BTreeMap::new();
for chain in &chains {
chains_by_output
.entry(chain.output_commodity_id.clone())
.or_default()
.push(chain.clone());
}
let systems = load_systems(conn);
let corp_presences = load_corp_presences(conn);
let mut presences_by_system: BTreeMap<String, Vec<CorpPresence>> = BTreeMap::new();
for cp in &corp_presences {
presences_by_system
.entry(cp.system_id.clone())
.or_default()
.push(cp.clone());
}
let gate_links = load_gate_links(conn);
let corp_archetype_data = load_corp_archetype_data(conn);
Economy {
commodities,
commodity_map,
chains,
chains_by_output,
systems,
corp_presences,
presences_by_system,
gate_links,
corp_archetype_data,
}
}