feat(atlas): add sync-wiki command, sync Gateway body catalog to wiki
New atlas subcommand: sync-wiki generates a Celestial Bodies section in wiki pages from DB body/station data. All fields rendered: orbit, ID, name, type, inhabited, population, mass, gravity, atmosphere, biome, hydrosphere, economy, settlement pattern, industrial corridor. Stations table includes docking class, governance, gate flag, districts. Moons render indented under their parent body (↳ notation). Section is idempotent — re-running overwrites the existing section. DB is source of truth, wiki is generated output. No import direction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,14 @@ enum Commands {
|
||||
},
|
||||
/// List systems that have no bodies yet
|
||||
Unfinished,
|
||||
/// Sync body/station data into wiki page for a system (or all systems)
|
||||
SyncWiki {
|
||||
/// System ID (omit for all systems with bodies)
|
||||
system_id: Option<String>,
|
||||
/// Path to wiki directory (default: wiki/star-systems/)
|
||||
#[arg(long, default_value = "wiki/star-systems")]
|
||||
wiki: String,
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1233,6 +1241,302 @@ fn cmd_unfinished(conn: &Connection) {
|
||||
println!("{}", serde_json::to_string_pretty(&result).unwrap());
|
||||
}
|
||||
|
||||
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,
|
||||
biome_summary, 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>,
|
||||
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)?,
|
||||
biome: row.get(10)?,
|
||||
hydro: row.get(11)?,
|
||||
econ: row.get(12)?,
|
||||
settlement: row.get(13)?,
|
||||
industrial: row.get(14)?,
|
||||
})
|
||||
})
|
||||
.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 | Population | Mass | Gravity | Atmosphere | Biome | Hydrosphere | 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());
|
||||
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,
|
||||
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());
|
||||
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,
|
||||
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 after the System Profile section (after the first "---" pair)
|
||||
// Find the second "---" which ends the System Profile
|
||||
let mut dashes = 0;
|
||||
let mut insert_pos = None;
|
||||
for (i, line) in content.lines().enumerate() {
|
||||
if line.trim() == "---" {
|
||||
dashes += 1;
|
||||
if dashes == 3 {
|
||||
// Third --- is end of System Profile
|
||||
let byte_pos: usize = content.lines().take(i + 1).map(|l| l.len() + 1).sum();
|
||||
insert_pos = Some(byte_pos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pos) = insert_pos {
|
||||
format!("{}\n{}\n---\n\n{}", &content[..pos], section.trim_end(), &content[pos..])
|
||||
} else {
|
||||
// Fallback: append at end
|
||||
format!("{}\n{}", content, 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()
|
||||
);
|
||||
}
|
||||
|
||||
fn format_population(pop: i64) -> String {
|
||||
if pop >= 1_000_000_000 {
|
||||
format!("{:.1}B", pop as f64 / 1_000_000_000.0)
|
||||
} else if pop >= 1_000_000 {
|
||||
format!("{}M", pop / 1_000_000)
|
||||
} else if pop >= 1_000 {
|
||||
format!("{}K", pop / 1_000)
|
||||
} else {
|
||||
format!("{}", pop)
|
||||
}
|
||||
}
|
||||
|
||||
// cmd_populate removed — replaced by per-system author/commit workflow
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1261,5 +1565,8 @@ fn main() {
|
||||
Commands::CommitSystem { path } => cmd_commit_system(&conn, path),
|
||||
Commands::WipeSystem { system_id } => cmd_wipe_system(&conn, system_id),
|
||||
Commands::Unfinished => cmd_unfinished(&conn),
|
||||
Commands::SyncWiki { system_id, wiki } => {
|
||||
cmd_sync_wiki(&conn, system_id.as_deref(), wiki)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,3 +66,23 @@ The Institute has been studying the first horizon station for five hundred years
|
||||
<!-- READ-ONLY — regenerated from star-map.json edges -->
|
||||
- **Hop Distance from Gateway:** 0
|
||||
- **Adjacent Systems:** GJ 0, GJ 559B, GJ 144, GJ 244A, GJ 725B
|
||||
|
||||
## Celestial Bodies
|
||||
<!-- READ-ONLY — generated from systems.db bodies/stations tables -->
|
||||
|
||||
| Orbit | ID | Name | Type | Inhabited | Population | Mass | Gravity | Atmosphere | Biome | Hydrosphere | Economy | Settlement | Industrial |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | `GJ71b` | — | planet | no | — | terrestrial | — | none | barren | none | — | — | — |
|
||||
| 2 | `GJ71c` | Threshold | planet | yes | 600M | terrestrial | — | breathable | temperate | ocean | agricultural | urban_concentrated | — |
|
||||
| 3 | `GJ71d` | Arden | planet | yes | 500M | terrestrial | — | breathable | temperate | rivers | agricultural | dispersed_rural | — |
|
||||
| ↳ 3.1 | `GJ71d-1` | Verantis | moon | yes | 20M | dwarf | — | none | barren | none | transit | domed | — |
|
||||
| 4 | `GJ71e` | — | planet | no | — | terrestrial | — | thin | frozen | ice | — | — | — |
|
||||
| 5 | `GJ71-belt` | — | asteroid_belt | no | — | — | — | — | — | — | — | — | — |
|
||||
| 6 | `GJ71-oort` | — | oort_cloud | no | — | — | — | — | — | — | — | — | — |
|
||||
|
||||
### Stations & Facilities
|
||||
|
||||
| ID | Name | Type | Orbits | Population | Economy | Governance | Docking | Gate | Districts |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| `GJ71-oort-S1` | Horizon Station | horizon | `GJ71-oort` | 80M | transit | — | major | yes | 1 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user