test(simulation): lock standalone-HQ-as-ordinary-settlement-row invariant (T-1074)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 10:29:17 +02:00
co-authored by Claude Fable 5
parent 62bbf57009
commit 314894d080
2 changed files with 114 additions and 0 deletions
+70
View File
@@ -828,6 +828,76 @@ mod tests {
assert!(p2.synthetic);
}
/// D-242: "hubs are the significant cities, standalone HQs are typically
/// minor nodes." `match_cities` has no concept of "corp-originated"
/// settlement — a Standalone-HQ CityRecord competes on population/class
/// exactly like an ordinary pooled city, so a modest-population HQ must
/// NOT out-rank a genuinely major city for the best attractor, and a
/// major HQ (large population, or NameLocked) DOES win Tier A on the same
/// terms any other settlement would. Both directions verified here.
#[test]
fn standalone_hq_settlement_competes_on_equal_terms_with_pooled_cities() {
let major_city = make_city(1, SettlementClass::PopulationBudget, 5_000_000);
// A "minor node" Standalone HQ (D-242's own framing) — modest
// population, ordinary PopulationBudget class, nothing marking it as
// corp-originated (mirrors populate_standalone_hq_settlements' output
// shape: an atlas_city_names row with no corp_id, same as any city).
let minor_hq = make_city(2, SettlementClass::PopulationBudget, 80_000);
let cities = vec![major_city, minor_hq];
let attractors = vec![
make_attractor(5, 5, AttractorType::RiverMouth, 95), // best
make_attractor(10, 10, AttractorType::ValleyFloor, 30), // worst
];
let matrix = uniform_matrix();
let placements = match_cities(
&cities,
&attractors,
&matrix,
None,
512,
256,
&TerritorialStatus::FrontierUnclaimed,
SeedChain::root(42),
);
let major = placements.iter().find(|p| p.city_id == 1).unwrap();
let minor = placements.iter().find(|p| p.city_id == 2).unwrap();
assert_eq!(
major.position,
(5, 5),
"the major city (5M pop) must win the best attractor over an 80k-pop HQ"
);
assert_eq!(
minor.position,
(10, 10),
"the minor HQ gets whatever's left — no special corp-origin priority"
);
// Flip it: a NameLocked HQ (the T-1075 hero-pin path — e.g. "The Gate
// Corporation" in the real data) DOES win Tier A, on the same
// NameLocked-priority terms as any hero-pinned city (tier_a_gets_priority,
// above) — population size never mattered for NameLocked, corp-origin
// doesn't add or subtract anything either.
let hero_hq = make_city(3, SettlementClass::NameLocked, 200_000);
let ordinary_city = make_city(4, SettlementClass::PopulationBudget, 3_000_000);
let cities2 = vec![hero_hq, ordinary_city];
let placements2 = match_cities(
&cities2,
&attractors,
&matrix,
None,
512,
256,
&TerritorialStatus::FrontierUnclaimed,
SeedChain::root(42),
);
let hero = placements2.iter().find(|p| p.city_id == 3).unwrap();
assert_eq!(
hero.position,
(5, 5),
"NameLocked HQ wins Tier A over a larger ordinary city — same rule as any NameLocked settlement"
);
}
#[test]
fn all_cities_placed() {
let cities: Vec<CityRecord> = (1..=5)
+44
View File
@@ -1154,6 +1154,50 @@ mod tests {
);
}
// ─── D-242 corp-HQ settlement model (T-1074/T-1075) ──────────────────────
//
// A Standalone-HQ settlement (economy_import/corporations.py:
// populate_standalone_hq_settlements) is inserted into atlas_city_names as
// an ORDINARY row — same schema, same fields, no corp linkage marker (the
// corp<->settlement relationship lives on corporations.headquarters_body,
// not on atlas_city_names). This module's test fixture schema
// (make_settlements_db, above) never had a corp_id column to begin with —
// this test makes that design invariant explicit: read_body_settlements
// (the D-211 placement pipeline's input) cannot distinguish a
// corp-Standalone-HQ row from a wiki-pooled city, by construction. If a
// future change ever needs to special-case corp-originated settlements,
// it will have to be threaded through explicitly — this test fails the
// moment that stops being true silently.
#[test]
fn read_body_settlements_treats_standalone_hq_row_identically_to_pooled_city() {
// "Gate Corporation" here stands in for a Phase-B-inserted
// Standalone-HQ row (name = corp proper_name, economic_role from
// corp_hq_placement.toml's standalone_economic_role, population from
// the T-1075 Zipf bake, settlement_class from the same bake/override
// path as any other city) — indistinguishable in shape from the
// ordinary pooled cities "Tributarium"/"Ruhr" it sits alongside.
let db = make_settlements_db(&[
("Tributarium", "manufacturing", 2_727_273_224, Some("PopulationBudget")),
("Ruhr", "manufacturing", 1_363_636_611, Some("PopulationBudget")),
("Gate Corporation", "manufacturing", 909_090_165, Some("NameLocked")),
]);
let reader = CityContextReader::open(&db).expect("open");
let cities = reader.read_body_settlements("PlanetX").expect("read");
assert_eq!(cities.len(), 3, "all three rows read back, HQ or not");
let hq = cities
.iter()
.find(|c| c.name == "Gate Corporation")
.expect("Standalone-HQ row must be present");
// Every CityRecord field the D-211 pipeline reads is populated exactly
// like an ordinary city's — nothing marks this row as corp-originated.
assert_eq!(hq.economic_role, "manufacturing");
assert_eq!(hq.population, 909_090_165);
assert_eq!(hq.settlement_class, SettlementClass::NameLocked);
assert!(!hq.is_capital, "Standalone HQ is not a capital by default");
}
// ─── read_body_city_names (T-949) ────────────────────────────────────────
#[test]