feat(simulation): Layer-3 settlement spatial-character enrichment (#956)

Derive and store each placed settlement's spatial character on top of the
#955 placement (D-212/213/214/215). Derivation is pure + integer-deterministic
(D-010); the cascade stays DB-free (D-225) — the enqueuer pre-resolves the
body's system faction onto the work item, like #955's settlements.

- TerritorialStatus (D-212): mapped from the authored dominant_faction
  (territorial_status_from_faction). Adds an AutonomistHeld variant for the
  Compact of Westphalia (self-governing bloc that rejects Assembly authority —
  neither Commission, Corp, Contested, nor truly Frontier). Grounded in
  wiki/factions/. Stored per-province on DrainageBasin.territorial_status
  (uniform per body for now; forward-compatible for per-province faction data).
- PoliticalArchetype (D-214): political_archetype(status, role), status takes
  precedence over economic_role.
- ArrangementPattern (D-215): new 5-variant enum + arrangement_pattern(); the
  block-adjacency *enforcement* stays deferred to the Quarter-skeleton gen (#957)
  — this only derives + stores which pattern applies.
- FoundingOrientation (D-213): existing fn extended with a seed-derived Free
  bearing so pioneer/open-terrain grids vary per seed.

Wiring: dominant_faction threaded through CityContextReader
(read_body_dominant_faction) → atlas proxy (cache-miss read) → AnalyzeBody work
item → run_cascade → run_layer3/match_cities. Enrichment stored on CityPlacement
(political_archetype, arrangement_pattern, founding_orientation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 14:57:09 +02:00
co-authored by Claude Opus 4.8
parent 8b1005221d
commit af60d85d94
9 changed files with 514 additions and 45 deletions
+29 -15
View File
@@ -95,21 +95,34 @@ pub fn handle_atlas_request(
// Miss — resolve the source heightmap and enqueue background analysis.
match resolver.resolve(&req.body_id) {
Ok(heightmap_path) => {
// Pre-resolve this body's settlements so the Rayon work item stays
// DB-free (#955, D-225). A read failure is non-fatal: log and place
// no cities (Layer 1 still runs).
let cities = match city_reader {
Some(reader) => reader
.read_body_settlements(&req.body_id)
.unwrap_or_else(|e| {
tracing::warn!(
body_id = %req.body_id,
error = %e,
"settlement read failed; placing no cities"
);
Vec::new()
}),
None => Vec::new(),
// Pre-resolve this body's settlements + system faction so the Rayon
// work item stays DB-free (#955/#956, D-225). Read failures are
// non-fatal: log and fall back (no cities / no faction → frontier).
let (cities, dominant_faction) = match city_reader {
Some(reader) => {
let cities = reader
.read_body_settlements(&req.body_id)
.unwrap_or_else(|e| {
tracing::warn!(
body_id = %req.body_id,
error = %e,
"settlement read failed; placing no cities"
);
Vec::new()
});
let faction = reader
.read_body_dominant_faction(&req.body_id)
.unwrap_or_else(|e| {
tracing::warn!(
body_id = %req.body_id,
error = %e,
"dominant_faction read failed; defaulting to frontier"
);
None
});
(cities, faction)
}
None => (Vec::new(), None),
};
queue.submit(
GenWorkItem::AnalyzeBody {
@@ -118,6 +131,7 @@ pub fn handle_atlas_request(
sea_level: DEFAULT_SEA_LEVEL,
body_seed: SeedChain::for_body(world_seed, &req.body_id),
cities,
dominant_faction,
},
GenPriority::Immediate,
);