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