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>
This commit is contained in:
2026-04-08 13:51:23 +02:00
co-authored by Claude Opus 4.6
parent dcd1c1cf4e
commit d45cfe0fa3
10 changed files with 241 additions and 201 deletions
+21 -19
View File
@@ -1,6 +1,6 @@
//! Database loading — reads economy data from systems.db.
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::process;
@@ -80,15 +80,15 @@ pub struct GateLink {
/// The complete economics dataset loaded from systems.db.
pub struct Economy {
pub commodities: Vec<Commodity>,
pub commodity_map: HashMap<String, 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: HashMap<String, Vec<ProductionChain>>,
pub chains_by_output: BTreeMap<String, Vec<ProductionChain>>,
/// Map: system_id → SystemInfo
pub systems: HashMap<String, SystemInfo>,
pub systems: BTreeMap<String, SystemInfo>,
pub corp_presences: Vec<CorpPresence>,
/// Map: system_id → list of corp presences
pub presences_by_system: HashMap<String, Vec<CorpPresence>>,
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?)
@@ -193,12 +193,12 @@ fn load_chains(conn: &Connection) -> Vec<ProductionChain> {
.collect();
// Build index of chain_id → inputs
let mut input_map: HashMap<String, Vec<ChainInput>> = HashMap::new();
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 });
input_map.entry(chain_id).or_default().push(ChainInput {
commodity_id,
quantity,
});
}
for chain in &mut chains {
@@ -210,7 +210,7 @@ fn load_chains(conn: &Connection) -> Vec<ProductionChain> {
chains
}
fn load_systems(conn: &Connection) -> HashMap<String, SystemInfo> {
fn load_systems(conn: &Connection) -> BTreeMap<String, SystemInfo> {
let mut stmt = conn
.prepare(
"SELECT ss.system_id, ss.proper_name, ss.cultural_corridor,
@@ -234,7 +234,9 @@ fn load_systems(conn: &Connection) -> HashMap<String, SystemInfo> {
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()),
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)?,
@@ -246,9 +248,7 @@ fn load_systems(conn: &Connection) -> HashMap<String, SystemInfo> {
.collect()
}
fn load_corp_archetype_data(
conn: &Connection,
) -> Vec<(String, Option<String>, Option<String>)> {
fn load_corp_archetype_data(conn: &Connection) -> Vec<(String, Option<String>, Option<String>)> {
let mut stmt = conn
.prepare(
"SELECT corp_id, behavioral_archetype, specialization
@@ -315,11 +315,13 @@ fn load_corp_presences(conn: &Connection) -> Vec<CorpPresence> {
pub fn load_economy(conn: &Connection) -> Economy {
let commodities = load_commodities(conn);
let commodity_map: HashMap<String, Commodity> =
commodities.iter().map(|c| (c.id.clone(), c.clone())).collect();
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: HashMap<String, Vec<ProductionChain>> = HashMap::new();
let mut chains_by_output: BTreeMap<String, Vec<ProductionChain>> = BTreeMap::new();
for chain in &chains {
chains_by_output
.entry(chain.output_commodity_id.clone())
@@ -330,7 +332,7 @@ pub fn load_economy(conn: &Connection) -> Economy {
let systems = load_systems(conn);
let corp_presences = load_corp_presences(conn);
let mut presences_by_system: HashMap<String, Vec<CorpPresence>> = HashMap::new();
let mut presences_by_system: BTreeMap<String, Vec<CorpPresence>> = BTreeMap::new();
for cp in &corp_presences {
presences_by_system
.entry(cp.system_id.clone())