feat(simulation): D-232 three-phase trait draw + deviation/swerve system (T-994, T-1003)
T-994 — three-phase draw replacing the flat splitmix64 flavor pick: - Phase 1 body K-draw (trait_draw.rs): hard gates -> soft weights (two-part geographic_sector join, PR #148) -> SeedChain::for_body-seeded weighted draw of K (5/3/1/0 by ComplexityTier), pins count toward K, coverage-aware over a new body-level district-type-mix aggregate threaded into CityGenerationContext at L3->L4 dispatch. - Phase 2 district-dominant pick keyed by the D-243 2048m District cell — settlements sharing a cell independently derive the identical template. Resolved at dispatch, never in FillChunk (T-987 stays pure). - trait_catalog_reader.rs: systems.db reader for trait_templates + atlas_body_trait_bias (D-225 resource pattern), registered in main.rs with graceful degradation. - BlockSkeleton carries district_type; assign_block_tags is a pure lookup. T-1003 — deviation/swerve system (trait_swerve.rs): - ArchitectureFlavorRef is now InVocabulary(u8) | Swerve(tag). - Rare per-building wildcard: foreign-import pool (other corridors + cross_corridor) driven by Epicenter/Passage tier, mixed faction, road-graph degree; heritage-callback pool (own-corridor heritage) driven by isolation, remote tier, founding_age_years. Integer bps, base 100, cap 300 per driver — placeholder constants pending calibration. - Sparsity escape hatch = same mechanism, necessity-triggered at the phase-2 pick (deterministic max-weight over the hard-gate-eligible pool). - New SeedDomains: TraitVocabulary(13) / TraitDistrict(14) / TraitSwerve(15). Governance: D-232 amended (district pinned to the D-243 2048m tier — the D-222/D-243 redefinition was never captured; swerve driver mappings + placeholder rates recorded), D-229 flavor_ref field updated to the enum shape, D-235 amended (T-995 vocabulary ratification note; rides here due to single-file entanglement). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,9 +23,18 @@
|
||||
//! - `founding_orientation` — Cardinal stub (attractor matching deferred)
|
||||
//! - `world_tier` — Waypoint default (`system_economy.economic_tier` derivation deferred)
|
||||
//! - `morphology_zone` — AlluvialPlain default (D-228 deferred)
|
||||
//! - `trait_selection` — empty (trait catalog #1005 deferred)
|
||||
//! - `trait_selection` — empty (the D-232 phase-1 K-draw runs at L3→L4 dispatch
|
||||
//! time, T-994 — see `atlas::plugin::build_skeleton_work_item` — this reader
|
||||
//! has no body-wide coverage view)
|
||||
//! - `dominant_bulk_class` — NonPhysical default (#982 design-blocked)
|
||||
//! - `dominant_production_ubiquity` — Common default (#982 design-blocked)
|
||||
//! - `body_district_type_mix` / `settlement_district_pos` / `district_dominant_by_type`
|
||||
//! — T-994 additions, all require body-wide aggregation this per-city reader
|
||||
//! doesn't have; overridden at L3→L4 dispatch time
|
||||
//!
|
||||
//! **Field read, not deferred (T-994 addition):** `geographic_sector` — from
|
||||
//! `star_systems.geographic_sector` (via `bodies.system_id`). A single
|
||||
//! per-system value, unlike the four fields above.
|
||||
//!
|
||||
//! **Prosperity derivation (D-197, partial — integer basis points, D-010):**
|
||||
//! `prosperity_baseline_bps = clamp(role_base_bps + pop_bonus_bps + noise_bps, 1000, 9500)`
|
||||
@@ -38,6 +47,7 @@
|
||||
//! Read-only `systems.db` access follows the same pattern as
|
||||
//! [`crate::atlas::source_resolver::BodySourceResolver`].
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@@ -90,6 +100,10 @@ pub struct CityEconomicReadSet {
|
||||
pub founding_age_years: u32,
|
||||
/// D-199 field 6.
|
||||
pub settlement_class: SettlementClass,
|
||||
/// Not a D-199 field — T-994 (D-232) addition. This city's system corridor
|
||||
/// (`star_systems.geographic_sector`). `None` if unset. A soft weight on the
|
||||
/// trait-template draw only, never a gate.
|
||||
pub geographic_sector: Option<String>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -136,26 +150,31 @@ impl CityContextReader {
|
||||
// atlas_city_names carries economic_role, population, settlement_class.
|
||||
// bodies carries founding_age_years (via the city's body_id).
|
||||
// system_factions carries dominant_faction (via bodies.system_id).
|
||||
// star_systems carries geographic_sector (via bodies.system_id, T-994).
|
||||
//
|
||||
// settlement_class is nullable (NULL until attractor placement runs).
|
||||
// dominant_faction is nullable (some systems have no faction data).
|
||||
// founding_age_years is nullable (uninhabited bodies).
|
||||
// geographic_sector is nullable (some systems have no recorded corridor).
|
||||
let row: rusqlite::Result<(
|
||||
Option<String>, // acn.economic_role
|
||||
i64, // acn.population
|
||||
Option<String>, // acn.settlement_class
|
||||
Option<i64>, // b.founding_age_years
|
||||
Option<String>, // sf.dominant_faction
|
||||
Option<String>, // ss.geographic_sector
|
||||
)> = conn.query_row(
|
||||
"SELECT
|
||||
acn.economic_role,
|
||||
acn.population,
|
||||
acn.settlement_class,
|
||||
b.founding_age_years,
|
||||
sf.dominant_faction
|
||||
sf.dominant_faction,
|
||||
ss.geographic_sector
|
||||
FROM atlas_city_names AS acn
|
||||
JOIN bodies AS b ON b.body_id = acn.body_id
|
||||
LEFT JOIN system_factions AS sf ON sf.system_id = b.system_id
|
||||
LEFT JOIN star_systems AS ss ON ss.system_id = b.system_id
|
||||
WHERE acn.id = ?1",
|
||||
[city_id as i64],
|
||||
|row| {
|
||||
@@ -165,6 +184,7 @@ impl CityContextReader {
|
||||
row.get(2)?,
|
||||
row.get(3)?,
|
||||
row.get(4)?,
|
||||
row.get(5)?,
|
||||
))
|
||||
},
|
||||
);
|
||||
@@ -183,6 +203,7 @@ impl CityContextReader {
|
||||
settlement_class_opt,
|
||||
founding_age_opt,
|
||||
dominant_faction,
|
||||
geographic_sector,
|
||||
) = row;
|
||||
|
||||
let economic_role = economic_role_opt.ok_or(CityContextReadError::MissingField {
|
||||
@@ -209,6 +230,7 @@ impl CityContextReader {
|
||||
dominant_faction,
|
||||
founding_age_years,
|
||||
settlement_class,
|
||||
geographic_sector,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -486,12 +508,33 @@ pub fn context_from_read_set(city_id: u64, rs: CityEconomicReadSet) -> CityGener
|
||||
world_tier: WorldTier::Waypoint,
|
||||
// morphology_zone: Layer-1 output, D-228 deferred.
|
||||
morphology_zone: MorphologyZone::AlluvialPlain,
|
||||
// trait_selection: trait catalog #1005 deferred.
|
||||
// trait_selection: the D-232 phase-1 K-draw runs at L3→L4 dispatch time
|
||||
// (build_skeleton_work_item, T-994) — this reader has no body-wide view
|
||||
// (coverage aggregate, catalog reader) so it is left empty here and
|
||||
// overridden by the caller, same pattern as morphology_zone/political_archetype.
|
||||
trait_selection: Vec::new(),
|
||||
// dominant_bulk_class: dominant-commodity derivation #982 design-blocked.
|
||||
dominant_bulk_class: BulkClass::NonPhysical,
|
||||
// dominant_production_ubiquity: same blocker as above.
|
||||
dominant_production_ubiquity: ProductionUbiquity::Common,
|
||||
|
||||
// ── T-994 additions ────────────────────────────────────────────────
|
||||
// geographic_sector: the one D-199-style read-set field this ticket adds
|
||||
// (star_systems.geographic_sector via the city's system_id) — real value,
|
||||
// not a stub.
|
||||
geographic_sector: rs.geographic_sector,
|
||||
// body_district_type_mix / settlement_district_pos / district_dominant_by_type:
|
||||
// all three require body-wide aggregation (every settlement's district
|
||||
// mix, the settlement's world position) that this per-city reader doesn't
|
||||
// have. Overridden at L3→L4 dispatch time (build_skeleton_work_item).
|
||||
body_district_type_mix: Vec::new(),
|
||||
settlement_district_pos: (0, 0),
|
||||
district_dominant_by_type: BTreeMap::new(),
|
||||
// ── T-1003 additions — same dispatch-time override story as above:
|
||||
// driver rates need the road graph, pools need the resolved catalog.
|
||||
swerve_rates_bps: (0, 0),
|
||||
swerve_foreign_pool: Vec::new(),
|
||||
swerve_heritage_pool: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,7 +582,7 @@ mod tests {
|
||||
let conn = Connection::open(&path).expect("create db");
|
||||
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE star_systems (system_id TEXT PRIMARY KEY);
|
||||
"CREATE TABLE star_systems (system_id TEXT PRIMARY KEY, geographic_sector TEXT);
|
||||
CREATE TABLE bodies (
|
||||
body_id TEXT PRIMARY KEY,
|
||||
system_id TEXT NOT NULL,
|
||||
@@ -734,6 +777,37 @@ mod tests {
|
||||
assert_eq!(rs.founding_age_years, 450);
|
||||
// Field 6 — settlement_class
|
||||
assert!(matches!(rs.settlement_class, SettlementClass::NameLocked));
|
||||
// geographic_sector: absent in this fixture (no UPDATE below) -> None.
|
||||
assert_eq!(rs.geographic_sector, None);
|
||||
}
|
||||
|
||||
// ─── geographic_sector (T-994) ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn read_set_threads_geographic_sector_from_star_systems() {
|
||||
let (db, city_id) = make_test_db(
|
||||
"GJ8d",
|
||||
"GJ-8",
|
||||
"financial",
|
||||
1_000_000,
|
||||
Some("NameLocked"),
|
||||
Some(100),
|
||||
None,
|
||||
);
|
||||
let conn = Connection::open(&db).expect("reopen");
|
||||
conn.execute(
|
||||
"UPDATE star_systems SET geographic_sector = 'east_reach' WHERE system_id = 'GJ-8'",
|
||||
[],
|
||||
)
|
||||
.expect("set sector");
|
||||
drop(conn);
|
||||
|
||||
let reader = CityContextReader::open(&db).expect("open");
|
||||
let rs = reader.read_set(city_id as u64, 42).expect("read set");
|
||||
assert_eq!(rs.geographic_sector.as_deref(), Some("east_reach"));
|
||||
|
||||
let ctx = context_from_read_set(city_id as u64, rs);
|
||||
assert_eq!(ctx.geographic_sector.as_deref(), Some("east_reach"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user