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
+30
View File
@@ -297,6 +297,36 @@ impl CityContextReader {
}
Ok(out)
}
/// Read the body's system `dominant_faction` (D-237) for Layer-3
/// TerritorialStatus + spatial-character derivation (#956). Joins
/// `bodies` → `system_factions` via `system_id`. Returns `None` when the
/// body is unknown or its system has no recorded faction (both → the
/// `FrontierUnclaimed` default downstream). Best-effort: only a DB/mutex
/// error fails.
pub fn read_body_dominant_faction(
&self,
body_id: &str,
) -> Result<Option<String>, CityContextReadError> {
let conn = self
.conn
.lock()
.map_err(|e| CityContextReadError::Db(format!("mutex poisoned: {e}")))?;
let result = conn.query_row(
"SELECT sf.dominant_faction
FROM bodies AS b
LEFT JOIN system_factions AS sf ON sf.system_id = b.system_id
WHERE b.body_id = ?1",
[body_id],
|row| row.get::<_, Option<String>>(0),
);
match result {
Ok(faction) => Ok(faction),
// Body not present → no faction (treated as frontier downstream).
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(CityContextReadError::Db(e.to_string())),
}
}
}
// ---------------------------------------------------------------------------