feat(simulation): canonical spatial scale ladder + body_radius_km (T-1077)

First phase of the D-243 re-scale. Adds server/src/atlas/scale.rs as the
single source of truth for the metric containment ladder (voxel 1m -> chunk
64m -> block 128m -> quarter 512m -> district 2048m -> region 204.8km), the
fixed integer addressing (ChunkPos -> DistrictPos -> RegionPos), and the
elastic seam regions_per_equator(R). Compile-time const asserts enforce the
nesting so the rungs cannot silently drift again — the structural fix for the
Q-110 failure mode (three files, three different region sizes).

Wires body_radius_km (D-204) through BodyParams + the reader: the single
body-specific input to the elastic seam.

No behavior change — foundation only. The rename/re-scale of the carrier
(RegionProfile -> DistrictProfile) and on-demand district derivation follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 22:04:50 +02:00
co-authored by Claude Opus 4.8
parent e39ba89064
commit 38054d4b12
6 changed files with 204 additions and 17 deletions
+34 -17
View File
@@ -16,9 +16,10 @@
//!
//! | Field | Source |
//! |-------|--------|
//! | `hydrosphere` | `bodies.hydrosphere` |
//! | `atmosphere` | `bodies.atmosphere` |
//! | `planet_class` | `bodies.planet_class` |
//! | `hydrosphere` | `bodies.hydrosphere` |
//! | `atmosphere` | `bodies.atmosphere` |
//! | `planet_class` | `bodies.planet_class` |
//! | `body_radius_km` | `bodies.body_radius_km` (D-204 — the D-243 elastic-seam input) |
//!
//! `tectonic_activity` is **not** in the current schema; `BodyParams.tectonic_activity`
//! is left `None` so the derivation falls back to `planet_class` as documented
@@ -91,26 +92,28 @@ impl BodyParamsReader {
.lock()
.map_err(|e| BodyParamsReadError::Db(format!("mutex poisoned: {e}")))?;
// All three columns are nullable; row absence is what signals UnknownBody.
// All columns are nullable; row absence is what signals UnknownBody.
// `tectonic_activity` is absent from the current schema — leave that
// BodyParams field None (derives from planet_class at derivation time).
let result: rusqlite::Result<(
Option<String>, // b.hydrosphere
Option<String>, // b.atmosphere
Option<String>, // b.planet_class
Option<f64>, // b.body_radius_km (D-204 — the D-243 elastic seam input)
)> = conn.query_row(
"SELECT
b.hydrosphere,
b.atmosphere,
b.planet_class
b.planet_class,
b.body_radius_km
FROM bodies AS b
WHERE b.body_id = ?1",
[body_id],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
);
match result {
Ok((hydrosphere, atmosphere, planet_class)) => Ok(BodyParams {
Ok((hydrosphere, atmosphere, planet_class, body_radius_km)) => Ok(BodyParams {
hydrosphere,
atmosphere,
planet_class,
@@ -120,6 +123,7 @@ impl BodyParamsReader {
// not at the body level. Leave at struct defaults (0.0).
region_latitude_deg: 0.0,
elevation_km: 0.0,
body_radius_km,
}),
Err(rusqlite::Error::QueryReturnedNoRows) => {
Err(BodyParamsReadError::UnknownBody(body_id.to_string()))
@@ -161,6 +165,7 @@ mod tests {
hydrosphere: Option<&str>,
atmosphere: Option<&str>,
planet_class: Option<&str>,
body_radius_km: Option<f64>,
) -> PathBuf {
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!("sr_bpr_{}_{n}.db", std::process::id()));
@@ -171,15 +176,16 @@ mod tests {
body_id TEXT PRIMARY KEY,
hydrosphere TEXT,
atmosphere TEXT,
planet_class TEXT
planet_class TEXT,
body_radius_km REAL
);",
)
.expect("create tables");
conn.execute(
"INSERT INTO bodies (body_id, hydrosphere, atmosphere, planet_class)
VALUES (?1, ?2, ?3, ?4)",
rusqlite::params![body_id, hydrosphere, atmosphere, planet_class],
"INSERT INTO bodies (body_id, hydrosphere, atmosphere, planet_class, body_radius_km)
VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![body_id, hydrosphere, atmosphere, planet_class, body_radius_km],
)
.expect("insert body");
@@ -189,13 +195,20 @@ mod tests {
#[test]
fn reads_all_columns_present() {
let db = make_test_db("GJ1c", Some("ocean"), Some("breathable"), Some("temperate"));
let db = make_test_db(
"GJ1c",
Some("ocean"),
Some("breathable"),
Some("temperate"),
Some(6371.0),
);
let reader = BodyParamsReader::open(&db).expect("open");
let params = reader.read_body_params("GJ1c").expect("read");
assert_eq!(params.hydrosphere.as_deref(), Some("ocean"));
assert_eq!(params.atmosphere.as_deref(), Some("breathable"));
assert_eq!(params.planet_class.as_deref(), Some("temperate"));
assert_eq!(params.body_radius_km, Some(6371.0));
// Per-region fields always start at 0.0 from the reader.
assert_eq!(params.region_latitude_deg, 0.0);
assert_eq!(params.elevation_km, 0.0);
@@ -205,18 +218,19 @@ mod tests {
#[test]
fn handles_all_null_columns() {
let db = make_test_db("GJ2b", None, None, None);
let db = make_test_db("GJ2b", None, None, None, None);
let reader = BodyParamsReader::open(&db).expect("open");
let params = reader.read_body_params("GJ2b").expect("read");
assert!(params.hydrosphere.is_none());
assert!(params.atmosphere.is_none());
assert!(params.planet_class.is_none());
assert!(params.body_radius_km.is_none());
}
#[test]
fn unknown_body_returns_error() {
let db = make_test_db("GJ3c", None, None, None);
let db = make_test_db("GJ3c", None, None, None, None);
let reader = BodyParamsReader::open(&db).expect("open");
let err = reader.read_body_params("ghost").expect_err("should fail");
assert!(matches!(err, BodyParamsReadError::UnknownBody(_)));
@@ -224,7 +238,7 @@ mod tests {
#[test]
fn read_is_deterministic() {
let db = make_test_db("GJ4d", Some("ice"), Some("thin"), Some("frozen"));
let db = make_test_db("GJ4d", Some("ice"), Some("thin"), Some("frozen"), Some(3389.5));
let reader = BodyParamsReader::open(&db).expect("open");
let p1 = reader.read_body_params("GJ4d").expect("first read");
let p2 = reader.read_body_params("GJ4d").expect("second read");
@@ -232,6 +246,8 @@ mod tests {
assert_eq!(p1.hydrosphere, p2.hydrosphere);
assert_eq!(p1.atmosphere, p2.atmosphere);
assert_eq!(p1.planet_class, p2.planet_class);
assert_eq!(p1.body_radius_km, p2.body_radius_km);
assert_eq!(p1.body_radius_km, Some(3389.5));
}
#[test]
@@ -250,11 +266,12 @@ mod tests {
body_id TEXT PRIMARY KEY,
hydrosphere TEXT,
atmosphere TEXT,
planet_class TEXT
planet_class TEXT,
body_radius_km REAL
-- orbital_period_days, axial_tilt_deg, spectral_class, star_type
-- deliberately absent to prove SELECT doesn't reference them
);
INSERT INTO bodies VALUES ('X', 'ocean', 'breathable', 'temperate');",
INSERT INTO bodies VALUES ('X', 'ocean', 'breathable', 'temperate', 6371.0);",
)
.expect("setup");
drop(conn);