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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user