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:
@@ -534,9 +534,21 @@ fn validate_coverage(
|
||||
commodity_ids: &BTreeSet<String>,
|
||||
conn: &Connection,
|
||||
) -> CoverageReport {
|
||||
// Track which commodities have at least one producer
|
||||
// Only track commodities that appear in at least one lore archetype's
|
||||
// commodity lists. Chain intermediates (e.g. fusion_fuel, lattice_substrate)
|
||||
// are implicitly covered through production chains, not direct assignment.
|
||||
let archetype_commodities: BTreeSet<String> = lore_archetypes
|
||||
.values()
|
||||
.flat_map(|a| {
|
||||
a.primary_commodities
|
||||
.iter()
|
||||
.chain(a.secondary_commodities.iter())
|
||||
.cloned()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut commodity_coverage: BTreeMap<String, usize> = BTreeMap::new();
|
||||
for cid in commodity_ids {
|
||||
for cid in commodity_ids.intersection(&archetype_commodities) {
|
||||
commodity_coverage.insert(cid.clone(), 0);
|
||||
}
|
||||
|
||||
@@ -559,8 +571,8 @@ fn validate_coverage(
|
||||
*system_coverage.entry(corp.system_id.clone()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// Find uncovered commodities (services excluded from Tier-3 production coverage
|
||||
// since they're produced by the archetype presence, not physical production)
|
||||
// Find uncovered commodities — only archetype-assignable ones are checked.
|
||||
// Chain intermediates not in any archetype are excluded (see filter above).
|
||||
let uncovered_commodities: Vec<String> = commodity_coverage
|
||||
.iter()
|
||||
.filter(|(_, &count)| count < 3)
|
||||
@@ -706,7 +718,7 @@ fn fill_coverage_gaps(
|
||||
&lore.typical_sectors[rng.random_range(0..lore.typical_sectors.len())]
|
||||
};
|
||||
|
||||
// Find a populated body in this sector
|
||||
// Find populated bodies in this sector (deterministic ordering)
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT b.body_id, b.system_id
|
||||
@@ -714,50 +726,40 @@ fn fill_coverage_gaps(
|
||||
JOIN star_systems ss ON b.system_id = ss.system_id
|
||||
WHERE b.inhabited = 1 AND b.population > 1000
|
||||
AND ss.geographic_sector = ?1
|
||||
ORDER BY RANDOM() LIMIT 1",
|
||||
ORDER BY b.body_id",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let loc: Option<(String, String)> = stmt
|
||||
let candidates: Vec<(String, String)> = stmt
|
||||
.query_map(rusqlite::params![sector], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})
|
||||
.unwrap()
|
||||
.filter_map(|r| r.ok())
|
||||
.next();
|
||||
.collect();
|
||||
|
||||
if let Some((loc_id, sys_id)) = loc {
|
||||
let behavioral = pick_behavioral_archetype(rng, &lore.behavioral_affinity);
|
||||
let name = names::generate_name(rng, sector, &lore.category);
|
||||
let corp_id = make_corp_id(&name);
|
||||
if candidates.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate enough to reach the 3-corp minimum
|
||||
let current_count = report.commodity_coverage.get(com_id).copied().unwrap_or(0);
|
||||
for _ in current_count..3 {
|
||||
let n = names::generate_name(rng, sector, &lore.category);
|
||||
let cid = make_corp_id(&n);
|
||||
generated.entry(cid).or_insert_with(|| GeneratedCorp {
|
||||
proper_name: n,
|
||||
lore_archetype: arch_id.to_string(),
|
||||
behavioral_archetype: pick_behavioral_archetype(
|
||||
rng,
|
||||
&lore.behavioral_affinity,
|
||||
),
|
||||
location_id: loc_id.clone(),
|
||||
location_type: "body".to_string(),
|
||||
system_id: sys_id.clone(),
|
||||
geographic_sector: sector.to_string(),
|
||||
brands: vec![],
|
||||
});
|
||||
}
|
||||
let (loc_id, sys_id) =
|
||||
&candidates[rng.random_range(0..candidates.len())];
|
||||
|
||||
generated.entry(corp_id).or_insert_with(|| GeneratedCorp {
|
||||
proper_name: name,
|
||||
// Generate exactly enough to reach the 3-corp minimum
|
||||
let current_count = report.commodity_coverage.get(com_id).copied().unwrap_or(0);
|
||||
for _ in current_count..3 {
|
||||
let n = names::generate_name(rng, sector, &lore.category);
|
||||
let cid = make_corp_id(&n);
|
||||
generated.entry(cid).or_insert_with(|| GeneratedCorp {
|
||||
proper_name: n,
|
||||
lore_archetype: arch_id.to_string(),
|
||||
behavioral_archetype: behavioral,
|
||||
location_id: loc_id,
|
||||
behavioral_archetype: pick_behavioral_archetype(
|
||||
rng,
|
||||
&lore.behavioral_affinity,
|
||||
),
|
||||
location_id: loc_id.clone(),
|
||||
location_type: "body".to_string(),
|
||||
system_id: sys_id,
|
||||
system_id: sys_id.clone(),
|
||||
geographic_sector: sector.to_string(),
|
||||
brands: vec![],
|
||||
});
|
||||
@@ -960,12 +962,16 @@ fn main() {
|
||||
println!(" {}: {}", sector, count);
|
||||
}
|
||||
|
||||
// Per D-175: both coverage rules are Phase 2 gate conditions — hard errors
|
||||
let mut coverage_failed = false;
|
||||
|
||||
if !report.uncovered_commodities.is_empty() {
|
||||
eprintln!(
|
||||
"\n WARNING: {} commodities below 3-corp minimum: {:?}",
|
||||
"\n ERROR: {} commodities below 3-corp minimum: {:?}",
|
||||
report.uncovered_commodities.len(),
|
||||
report.uncovered_commodities
|
||||
);
|
||||
coverage_failed = true;
|
||||
}
|
||||
if !report.uncovered_systems.is_empty() {
|
||||
eprintln!(
|
||||
@@ -973,7 +979,9 @@ fn main() {
|
||||
report.uncovered_systems.len(),
|
||||
&report.uncovered_systems[..report.uncovered_systems.len().min(10)]
|
||||
);
|
||||
// Per D-175: coverage validation failures are hard errors
|
||||
coverage_failed = true;
|
||||
}
|
||||
if coverage_failed {
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user