fix(simulation): address PR #108 review — LEFT JOIN, JSON output, atmosphere default

Review fixes:
- corridor-status: INNER JOIN → LEFT JOIN so systems without gate
  records are included in counts instead of silently excluded
- corridor-status: output as JSON (serde_json) matching all other
  atlas commands, instead of plain-text ASCII table
- generate_body_matrix: emit atmosphere "standard" instead of
  "breathable" to match committed-system conventions
- Doc header: add corridor-status usage example
- CHANGELOG: note stale habitable_planet_count in pre-fix systems

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 23:50:11 +02:00
co-authored by Claude Opus 4.6
parent 6763c30d43
commit 16b7fe0bb5
2 changed files with 40 additions and 27 deletions
+3 -1
View File
@@ -10,7 +10,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
- `corridor-status` subcommand for atlas CLI — shows remaining unfinished systems grouped by geographic sector and hop distance (#744)
### Fixed
- `habitable_planet_count` filter now accepts both "breathable" and "standard" atmosphere values — previously all committed systems reported 0 habitable planets (#762)
- `habitable_planet_count` filter now accepts both "breathable" and "standard" atmosphere values — previously all committed systems reported 0 habitable planets (#762). Systems committed before this fix may have stale `habitable_planet_count = 0`; re-commit to update.
- `corridor-status` uses LEFT JOIN so systems without gate records are included in counts
- `generate_body_matrix` now emits `atmosphere: "standard"` (was "breathable") to match committed-system conventions
## [v0.1.29] — 2026-04-03
+37 -26
View File
@@ -10,6 +10,7 @@
//! tooling/atlas show-body "GJ 15Ab"
//! tooling/atlas add-body --system "GJ 15A" --type planet --orbit 1 --id "GJ 15Ab"
//! tooling/atlas stats
//! tooling/atlas corridor-status # remaining systems by corridor/hop
//! tooling/atlas populate # bulk classifier pass
//!
//! # Direct:
@@ -911,7 +912,7 @@ fn generate_body_matrix(
surface_gravity: Some(0.9),
orbital_period_days: Some(365.0),
rotation_period_hours: Some(24.0),
atmosphere: Some("breathable".into()),
atmosphere: Some("standard".into()),
biome_summary: Some("temperate".into()),
hydrosphere: None,
economic_role: None,
@@ -943,7 +944,7 @@ fn generate_body_matrix(
surface_gravity: Some(0.85),
orbital_period_days: Some(400.0),
rotation_period_hours: Some(26.0),
atmosphere: Some("breathable".into()),
atmosphere: Some("standard".into()),
biome_summary: Some("temperate".into()),
hydrosphere: None,
economic_role: None,
@@ -1999,26 +2000,28 @@ fn format_population(pop: i64) -> String {
fn cmd_corridor_status(conn: &Connection) {
// Query unfinished systems grouped by geographic_sector and hop_distance_from_gateway.
// "Unfinished" = no rows in bodies for this system_id.
// LEFT JOIN so systems with no gate record are still counted (hop = NULL → "?").
let mut stmt = conn
.prepare(
"SELECT s.geographic_sector, g.hop_distance_from_gateway, COUNT(*) AS remaining
FROM star_systems s
JOIN system_gates g ON s.system_id = g.system_id
LEFT JOIN system_gates g ON s.system_id = g.system_id
WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)
GROUP BY s.geographic_sector, g.hop_distance_from_gateway
ORDER BY s.geographic_sector, g.hop_distance_from_gateway",
)
.unwrap();
struct Row {
#[derive(Serialize)]
struct CorridorRow {
sector: Option<String>,
hop: Option<i32>,
remaining: i64,
}
let rows: Vec<Row> = stmt
let rows: Vec<CorridorRow> = stmt
.query_map([], |row| {
Ok(Row {
Ok(CorridorRow {
sector: row.get(0)?,
hop: row.get(1)?,
remaining: row.get(2)?,
@@ -2029,31 +2032,39 @@ fn cmd_corridor_status(conn: &Connection) {
.collect();
if rows.is_empty() {
println!("All systems have bodies authored.");
#[derive(Serialize)]
struct EmptyResult {
total: i64,
message: &'static str,
corridors: Vec<()>,
}
println!(
"{}",
serde_json::to_string_pretty(&EmptyResult {
total: 0,
message: "All systems have bodies authored.",
corridors: vec![],
})
.unwrap()
);
return;
}
// Plain text table: corridor | hop | remaining
let total: i64 = rows.iter().map(|r| r.remaining).sum();
println!("{:<30} {:>4} {:>9}", "Corridor", "Hop", "Remaining");
println!("{}", "-".repeat(48));
let mut last_sector = String::new();
for row in &rows {
let sector = row.sector.as_deref().unwrap_or("(unknown)");
let hop = row
.hop
.map(|h| h.to_string())
.unwrap_or_else(|| "?".to_string());
if sector != last_sector {
if !last_sector.is_empty() {
println!();
}
last_sector = sector.to_string();
}
println!("{:<30} {:>4} {:>9}", sector, hop, row.remaining);
#[derive(Serialize)]
struct CorridorResult {
total: i64,
corridors: Vec<CorridorRow>,
}
println!("{}", "-".repeat(48));
println!("{:<30} {:>4} {:>9}", "TOTAL", "", total);
println!(
"{}",
serde_json::to_string_pretty(&CorridorResult {
total,
corridors: rows,
})
.unwrap()
);
}
// ---------------------------------------------------------------------------