Files
settled-reach/server/src/bin/atlas/sync_wiki.rs
T
jpmschweitzerandClaude Opus 4.6 db241b88bd refactor(schema): rename biome_summary to planet_class (D-188)
"Biome" describes per-zone vegetation classification (Whittaker table).
"Planet class" describes overall planetary character. The conflation
caused the planet generator to misclassify ~270 bodies as barren.

Scope: systems.db column, schema SQL, Rust atlas code, wiki table
headers (Biome → Class), atlas proposal JSONs, all docs/decisions,
tooling scripts. Also normalizes atmosphere vocabulary (breathable →
standard) and expands planet class mapping to all 26 wiki values.
Unknown classes default to temperate for modder safety.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 17:10:35 +02:00

320 lines
12 KiB
Rust

use rusqlite::{params, Connection};
use serde::Serialize;
use crate::common::format_population;
pub fn cmd_sync_wiki(conn: &Connection, system_id: Option<&str>, wiki_dir: &str) {
// Get list of systems to sync
let system_ids: Vec<String> = if let Some(sid) = system_id {
vec![sid.to_string()]
} else {
let mut stmt = conn
.prepare("SELECT DISTINCT system_id FROM bodies ORDER BY system_id")
.unwrap();
stmt.query_map([], |row| row.get::<_, String>(0))
.unwrap()
.filter_map(|r| r.ok())
.collect()
};
let mut synced = 0;
for sid in &system_ids {
let sid_slug = sid.replace(' ', "-");
let wiki_path = std::path::Path::new(wiki_dir)
.join(&sid_slug)
.join("index.md");
if !wiki_path.exists() {
eprintln!("skip: {} — no wiki page at {}", sid, wiki_path.display());
continue;
}
// Query bodies
let mut body_stmt = conn
.prepare(
"SELECT body_id, proper_name, body_type, orbit_index, parent_body_id,
inhabited, population, mass_class, atmosphere, surface_gravity,
orbital_period_days, rotation_period_hours,
planet_class, hydrosphere, economic_role, settlement_pattern,
industrial_corridor
FROM bodies WHERE system_id = ?1
ORDER BY CASE WHEN parent_body_id IS NULL THEN orbit_index ELSE 1000 + orbit_index END",
)
.unwrap();
struct Body {
id: String,
name: Option<String>,
btype: String,
orbit: i32,
parent: Option<String>,
inhabited: bool,
population: i64,
mass_class: Option<String>,
atmosphere: Option<String>,
gravity: Option<f64>,
orbital_days: Option<f64>,
rotation_hours: Option<f64>,
biome: Option<String>,
hydro: Option<String>,
econ: Option<String>,
settlement: Option<String>,
industrial: Option<String>,
}
let bodies: Vec<Body> = body_stmt
.query_map(params![sid], |row| {
Ok(Body {
id: row.get(0)?,
name: row.get(1)?,
btype: row.get(2)?,
orbit: row.get::<_, Option<i32>>(3)?.unwrap_or(0),
parent: row.get(4)?,
inhabited: row.get::<_, i32>(5)? != 0,
population: row.get::<_, Option<i64>>(6)?.unwrap_or(0),
mass_class: row.get(7)?,
atmosphere: row.get(8)?,
gravity: row.get(9)?,
orbital_days: row.get(10)?,
rotation_hours: row.get(11)?,
biome: row.get(12)?,
hydro: row.get(13)?,
econ: row.get(14)?,
settlement: row.get(15)?,
industrial: row.get(16)?,
})
})
.unwrap()
.filter_map(|r| r.ok())
.collect();
// Query stations
let mut station_stmt = conn
.prepare(
"SELECT station_id, proper_name, orbits_body_id, station_type,
population, economic_role, governance_type, docking_class,
has_gate_infrastructure, district_count
FROM stations WHERE system_id = ?1
ORDER BY station_id",
)
.unwrap();
struct Station {
id: String,
name: Option<String>,
orbits: Option<String>,
stype: String,
population: i64,
econ: Option<String>,
governance: Option<String>,
docking: Option<String>,
gate: bool,
districts: i32,
}
let stations: Vec<Station> = station_stmt
.query_map(params![sid], |row| {
Ok(Station {
id: row.get(0)?,
name: row.get(1)?,
orbits: row.get(2)?,
stype: row.get(3)?,
population: row.get::<_, Option<i64>>(4)?.unwrap_or(0),
econ: row.get(5)?,
governance: row.get(6)?,
docking: row.get(7)?,
gate: row.get::<_, Option<i32>>(8)?.unwrap_or(0) != 0,
districts: row.get::<_, Option<i32>>(9)?.unwrap_or(1),
})
})
.unwrap()
.filter_map(|r| r.ok())
.collect();
if bodies.is_empty() && stations.is_empty() {
continue;
}
// Build the Celestial Bodies section
let mut section = String::new();
section.push_str("## Celestial Bodies\n");
section
.push_str("<!-- READ-ONLY — generated from systems.db bodies/stations tables -->\n\n");
// Bodies table
if !bodies.is_empty() {
section.push_str("| Orbit | ID | Name | Type | Inhabited | Pop | Mass | Gravity | Year (d) | Day (h) | Atmo | Biome | Hydro | Economy | Settlement | Industrial |\n");
section.push_str("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n");
// First pass: top-level bodies (no parent)
for b in &bodies {
if b.parent.is_some() {
continue;
}
let name = b.name.as_deref().unwrap_or("—");
let pop = if b.population > 0 {
format_population(b.population)
} else {
"—".to_string()
};
let grav = b
.gravity
.map(|g| format!("{:.2}g", g))
.unwrap_or_else(|| "—".to_string());
let year = b
.orbital_days
.map(|d| format!("{:.0}", d))
.unwrap_or_else(|| "—".to_string());
let day = b
.rotation_hours
.map(|h| format!("{:.1}", h))
.unwrap_or_else(|| "—".to_string());
section.push_str(&format!(
"| {} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
b.orbit,
b.id,
name,
b.btype,
if b.inhabited { "yes" } else { "no" },
pop,
b.mass_class.as_deref().unwrap_or("—"),
grav,
year,
day,
b.atmosphere.as_deref().unwrap_or("—"),
b.biome.as_deref().unwrap_or("—"),
b.hydro.as_deref().unwrap_or("—"),
b.econ.as_deref().unwrap_or("—"),
b.settlement.as_deref().unwrap_or("—"),
b.industrial.as_deref().unwrap_or("—"),
));
// Child bodies (moons)
for m in &bodies {
if m.parent.as_deref() == Some(&b.id) {
let mname = m.name.as_deref().unwrap_or("—");
let mpop = if m.population > 0 {
format_population(m.population)
} else {
"—".to_string()
};
let mgrav = m
.gravity
.map(|g| format!("{:.2}g", g))
.unwrap_or_else(|| "—".to_string());
let myear = m
.orbital_days
.map(|d| format!("{:.0}", d))
.unwrap_or_else(|| "—".to_string());
let mday = m
.rotation_hours
.map(|h| format!("{:.1}", h))
.unwrap_or_else(|| "—".to_string());
section.push_str(&format!(
"| ↳ {}.{} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n",
b.orbit,
m.orbit,
m.id,
mname,
m.btype,
if m.inhabited { "yes" } else { "no" },
mpop,
m.mass_class.as_deref().unwrap_or("—"),
mgrav,
myear,
mday,
m.atmosphere.as_deref().unwrap_or("—"),
m.biome.as_deref().unwrap_or("—"),
m.hydro.as_deref().unwrap_or("—"),
m.econ.as_deref().unwrap_or("—"),
m.settlement.as_deref().unwrap_or("—"),
m.industrial.as_deref().unwrap_or("—"),
));
}
}
}
section.push('\n');
}
// Stations table
if !stations.is_empty() {
section.push_str("### Stations & Facilities\n\n");
section.push_str("| ID | Name | Type | Orbits | Population | Economy | Governance | Docking | Gate | Districts |\n");
section.push_str("|---|---|---|---|---|---|---|---|---|---|\n");
for s in &stations {
let name = s.name.as_deref().unwrap_or("—");
let orbits = s.orbits.as_deref().unwrap_or("—");
let pop = if s.population > 0 {
format_population(s.population)
} else {
"—".to_string()
};
section.push_str(&format!(
"| `{}` | {} | {} | `{}` | {} | {} | {} | {} | {} | {} |\n",
s.id,
name,
s.stype,
orbits,
pop,
s.econ.as_deref().unwrap_or("—"),
s.governance.as_deref().unwrap_or("—"),
s.docking.as_deref().unwrap_or("—"),
if s.gate { "yes" } else { "no" },
s.districts,
));
}
section.push('\n');
}
// Read current wiki content
let content = std::fs::read_to_string(&wiki_path).unwrap();
// Replace or insert the Celestial Bodies section
let marker_start = "## Celestial Bodies";
let new_content = if let Some(start_pos) = content.find(marker_start) {
// Find the next ## heading after the section (or end of file)
let after = &content[start_pos + marker_start.len()..];
let end_offset = after
.find("\n## ")
.map(|p| start_pos + marker_start.len() + p + 1)
.unwrap_or(content.len());
format!(
"{}{}\n{}",
&content[..start_pos],
section.trim_end(),
&content[end_offset..]
)
} else {
// Insert before ## Topology if it exists, otherwise before end of file
if let Some(topo_pos) = content.find("\n## Topology") {
let insert_pos = topo_pos + 1; // after the newline
format!(
"{}{}\n\n{}",
&content[..insert_pos],
section.trim_end(),
&content[insert_pos..]
)
} else {
// No topology section — append at end
format!("{}\n{}", content.trim_end(), section)
}
};
std::fs::write(&wiki_path, new_content).unwrap();
eprintln!("synced: {}{}", sid, wiki_path.display());
synced += 1;
}
#[derive(Serialize)]
struct SyncResult {
systems_synced: usize,
}
println!(
"{}",
serde_json::to_string_pretty(&SyncResult {
systems_synced: synced
})
.unwrap()
);
}