feat(simulation): feature-name pipeline wired + legacy window_granularity u32 retired (T-1169, T-1159)

One commit for two tickets whose changes share the bridge/plugin
plumbing files. T-1169 connects the three dormant feature-name pieces:
atlas_feature_names populated at regen (17,891 rows — 15,190 mountain,
2,701 river — via populate_atlas_feature_names mirroring the city-names
importer; systems.db regenerated, stamp fresh), attach_feature_names
wired into the cascade's Topography block with name pools threaded
DB-free through AnalyzeBody (D-225 pattern) and assignments stored on
Layer1Output/BodyWorldState for future consumers, and a
FeatureNamesRequest/Response read proxy as the bridge's 7th tagged
envelope (D-236 pattern, both SimBridge impls). Client label DRAW is
deliberately NOT here — implementation proved both river and mountain
labels need a wire-carried position (the pool is position-free; course
polylines aren't correlated with the named attractors by construction) —
deferred to T-1195's single design pass. cascade_layer1 golden re-pinned
(additive feature_names field).

T-1159 retires the legacy u32 granularity field fully shadowed by
window_granularity_v2: AtlasLayerRequest.window_granularity,
DistrictWindowLayer.granularity echo, the u32::MAX sentinel, and
resolve_window_granularity are gone server-side; client encode paths and
the caller-less atlas_window_cache legacy key component dropped;
msgpack fixtures regenerated; the T-1150 aliasing regression test now
drives through the surviving enum field. The district_window carrier
itself survives byte-compatible per D-255(c).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 16:10:27 +02:00
co-authored by Claude Fable 5
parent befdb689c1
commit 8da9670e0f
28 changed files with 1244 additions and 467 deletions
+121
View File
@@ -467,6 +467,58 @@ impl CityContextReader {
}
Ok(out)
}
/// Read every reserved geographic feature **name** on `body_id` from
/// `atlas_feature_names` (T-1169 — mirrors [`read_body_city_names`]
/// exactly, D-236 pattern, over `atlas_feature_names` instead of
/// `atlas_city_names`). Returns the id/name/feature_type triple — this is
/// the raw reserved-name POOL, not a position assignment (positions come
/// from `layer1::attach_feature_names` at cascade generation time, not
/// from this reader). Ordered by `id`. An unknown body yields an empty
/// list (matches `read_body_city_names`'s convention); only a DB/mutex
/// error fails. **No Sol check here** — unlike city names, this is a raw
/// pool read with no per-body caller-facing status enum; the proxy
/// handler (`atlas_data_proxy::handle_feature_names_request`) applies the
/// same D-236 Sol exclusion `handle_city_names_request` does, BEFORE
/// calling this method, so Sol bodies never reach this query in practice.
pub fn read_body_feature_names(
&self,
body_id: &str,
) -> Result<Vec<FeatureNameRow>, CityContextReadError> {
let conn = self
.conn
.lock()
.map_err(|e| CityContextReadError::Db(format!("mutex poisoned: {e}")))?;
let mut stmt = conn
.prepare(
"SELECT id, name, feature_type
FROM atlas_feature_names
WHERE body_id = ?1
ORDER BY id",
)
.map_err(|e| CityContextReadError::Db(e.to_string()))?;
let rows = stmt
.query_map([body_id], |row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
))
})
.map_err(|e| CityContextReadError::Db(e.to_string()))?;
let mut out = Vec::new();
for r in rows {
let (id, name, feature_type) =
r.map_err(|e| CityContextReadError::Db(e.to_string()))?;
out.push(FeatureNameRow {
feature_id: id as u64,
name,
feature_type,
});
}
Ok(out)
}
}
/// One row of the T-949 names-only read (see
@@ -478,6 +530,20 @@ pub struct CityNameRow {
pub is_capital: bool,
}
/// One row of the T-1169 names-only read (see
/// [`CityContextReader::read_body_feature_names`]).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FeatureNameRow {
pub feature_id: u64,
pub name: String,
/// `atlas_feature_names.feature_type` — `"river"` | `"mountain"` (the two
/// pools `import_economics`/`atlas.py::populate_atlas_feature_names`
/// currently populates, T-1169 scope). Carried as a raw string, not an
/// enum — mirrors `CityNameRow.is_capital`'s discipline of staying a thin
/// passthrough of the DB row, no server-side vocabulary gate here.
pub feature_type: String,
}
// ---------------------------------------------------------------------------
// prosperity_baseline_bps derivation (D-197, partial — integer basis points)
// ---------------------------------------------------------------------------
@@ -1355,6 +1421,61 @@ mod tests {
);
}
// ─── read_body_feature_names (T-1169) ────────────────────────────────────
/// Minimal db for `read_body_feature_names`: just `atlas_feature_names`.
fn make_features_db(rows: &[(&str, &str)]) -> PathBuf {
let n = SEQ.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!("sr_ctxfeat_{}_{n}.db", std::process::id()));
let _ = std::fs::remove_file(&path);
let conn = Connection::open(&path).expect("create db");
conn.execute_batch(
"CREATE TABLE atlas_feature_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
body_id TEXT NOT NULL,
name TEXT NOT NULL,
feature_type TEXT NOT NULL
);",
)
.expect("create table");
for (name, feature_type) in rows {
conn.execute(
"INSERT INTO atlas_feature_names (body_id, name, feature_type)
VALUES ('PlanetX', ?1, ?2)",
rusqlite::params![name, feature_type],
)
.expect("insert feature");
}
drop(conn);
path
}
#[test]
fn read_body_feature_names_returns_id_name_type() {
let db = make_features_db(&[("Wiesenbach", "mountain"), ("Kaltfluss", "river")]);
let reader = CityContextReader::open(&db).expect("open");
let names = reader.read_body_feature_names("PlanetX").expect("read");
assert_eq!(names.len(), 2);
// Ordered by id == insertion order.
assert_eq!(names[0].name, "Wiesenbach");
assert_eq!(names[0].feature_type, "mountain");
assert_eq!(names[1].name, "Kaltfluss");
assert_eq!(names[1].feature_type, "river");
}
#[test]
fn read_body_feature_names_unknown_body_is_empty() {
let db = make_features_db(&[("Solo Peak", "mountain")]);
let reader = CityContextReader::open(&db).expect("open");
assert!(
reader
.read_body_feature_names("Ghost")
.expect("read")
.is_empty(),
"unknown body yields no names, matching read_body_city_names' convention"
);
}
// ─── is_sol_body (T-949, D-236) ──────────────────────────────────────────
/// Minimal db for `is_sol_body`: one `bodies` row + an optional