feat(atlas): add list-systems command and --sector filter to unfinished/next

New `list-systems` subcommand with --sector, --hop, --finished, --unfinished
filters. Also adds --sector flag to existing `unfinished` and `next` commands
for corridor-scoped queries during atlas authoring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 12:26:36 +02:00
co-authored by Claude Opus 4.6
parent c47a503c1f
commit 27343ea003
2 changed files with 262 additions and 38 deletions
+26
View File
@@ -37,6 +37,32 @@ Returns: body details + stations orbiting that body.
tooling/atlas show-station "GJ15Ab-S1"
```
### List systems with filters
```bash
tooling/atlas list-systems
tooling/atlas list-systems --sector west_reach
tooling/atlas list-systems --sector west_reach --hop 7
tooling/atlas list-systems --sector west_reach --unfinished
tooling/atlas list-systems --sector east_reach --finished
```
Sectors: `north_reach`, `south_reach`, `east_reach`, `west_reach`, `deep_frontier`, `core`
Returns: system_id, proper_name, star_type, spectral_class, gate_topology,
geographic_sector, hop_distance, population. Sorted by hop then system_id.
### Unfinished systems (with optional sector filter)
```bash
tooling/atlas unfinished
tooling/atlas unfinished --sector west_reach
```
### Next hop (with optional sector filter)
```bash
tooling/atlas next
tooling/atlas next 7
tooling/atlas next --sector west_reach
tooling/atlas next 8 --sector west_reach
```
### List bodies with filters
```bash
tooling/atlas list-bodies
+236 -38
View File
@@ -145,12 +145,34 @@ enum Commands {
/// System ID
system_id: String,
},
/// List systems with optional filters
ListSystems {
/// Filter by geographic sector (e.g., "west_reach", "east_reach")
#[arg(long)]
sector: Option<String>,
/// Filter by hop distance
#[arg(long)]
hop: Option<i32>,
/// Only show systems that have bodies authored
#[arg(long)]
finished: bool,
/// Only show systems that have NO bodies yet
#[arg(long)]
unfinished: bool,
},
/// List systems that have no bodies yet
Unfinished,
Unfinished {
/// Filter by geographic sector
#[arg(long)]
sector: Option<String>,
},
/// List unfinished systems at a specific hop distance (or next available hop)
Next {
/// Hop distance (omit to find the lowest hop with unfinished systems)
hop: Option<i32>,
/// Filter by geographic sector
#[arg(long)]
sector: Option<String>,
},
/// Sync body/station data into wiki page for a system (or all systems)
SyncWiki {
@@ -1349,15 +1371,130 @@ fn cmd_wipe_system(conn: &Connection, system_id: &str) {
println!("{}", serde_json::to_string_pretty(&result).unwrap());
}
fn cmd_unfinished(conn: &Connection) {
let mut stmt = conn
.prepare(
"SELECT s.system_id, s.proper_name, s.star_type
FROM star_systems s
WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)
ORDER BY s.system_id",
)
.unwrap();
fn cmd_list_systems(
conn: &Connection,
sector: Option<&str>,
hop: Option<i32>,
finished: bool,
unfinished: bool,
) {
// Build query dynamically based on filters
let mut conditions: Vec<String> = Vec::new();
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
let mut idx = 1;
if let Some(s) = sector {
conditions.push(format!("s.geographic_sector = ?{idx}"));
param_values.push(Box::new(s.to_string()));
idx += 1;
}
if let Some(h) = hop {
conditions.push(format!("g.hop_distance_from_gateway = ?{idx}"));
param_values.push(Box::new(h));
idx += 1;
}
let _ = idx; // suppress unused warning
if finished {
conditions.push(
"s.system_id IN (SELECT DISTINCT system_id FROM bodies)".to_string(),
);
}
if unfinished {
conditions.push(
"s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)".to_string(),
);
}
let where_clause = if conditions.is_empty() {
String::new()
} else {
format!(" WHERE {}", conditions.join(" AND "))
};
let sql = format!(
"SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class,
g.gate_topology, s.geographic_sector,
g.hop_distance_from_gateway,
s.habitable_planet_count, s.inhabited_planet_count,
e.population
FROM star_systems s
JOIN system_gates g ON s.system_id = g.system_id
LEFT JOIN system_economy e ON s.system_id = e.system_id
{where_clause}
ORDER BY g.hop_distance_from_gateway, s.system_id"
);
let mut stmt = conn.prepare(&sql).unwrap();
let params_refs: Vec<&dyn rusqlite::types::ToSql> =
param_values.iter().map(|p| p.as_ref()).collect();
#[derive(Serialize)]
struct ListSystem {
system_id: String,
proper_name: Option<String>,
star_type: Option<String>,
spectral_class: Option<String>,
gate_topology: Option<String>,
geographic_sector: Option<String>,
hop_distance: Option<i32>,
habitable_planet_count: Option<i32>,
inhabited_planet_count: Option<i32>,
population: Option<i64>,
}
let systems: Vec<ListSystem> = stmt
.query_map(params_refs.as_slice(), |row| {
Ok(ListSystem {
system_id: row.get(0)?,
proper_name: row.get(1)?,
star_type: row.get(2)?,
spectral_class: row.get(3)?,
gate_topology: row.get(4)?,
geographic_sector: row.get(5)?,
hop_distance: row.get(6)?,
habitable_planet_count: row.get(7)?,
inhabited_planet_count: row.get(8)?,
population: row.get(9)?,
})
})
.unwrap()
.filter_map(|r| r.ok())
.collect();
#[derive(Serialize)]
struct ListResult {
count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
sector: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
hop: Option<i32>,
systems: Vec<ListSystem>,
}
let result = ListResult {
count: systems.len(),
sector: sector.map(|s| s.to_string()),
hop,
systems,
};
println!("{}", serde_json::to_string_pretty(&result).unwrap());
}
fn cmd_unfinished(conn: &Connection, sector: Option<&str>) {
let sql = if sector.is_some() {
"SELECT s.system_id, s.proper_name, s.star_type
FROM star_systems s
WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)
AND s.geographic_sector = ?1
ORDER BY s.system_id"
} else {
"SELECT s.system_id, s.proper_name, s.star_type
FROM star_systems s
WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)
ORDER BY s.system_id"
};
let mut stmt = conn.prepare(sql).unwrap();
#[derive(Serialize)]
struct UnfinishedSystem {
@@ -1366,8 +1503,8 @@ fn cmd_unfinished(conn: &Connection) {
star_type: Option<String>,
}
let rows: Vec<UnfinishedSystem> = stmt
.query_map([], |row| {
let rows: Vec<UnfinishedSystem> = if let Some(s) = sector {
stmt.query_map(params![s], |row| {
Ok(UnfinishedSystem {
system_id: row.get(0)?,
proper_name: row.get(1)?,
@@ -1376,33 +1513,56 @@ fn cmd_unfinished(conn: &Connection) {
})
.unwrap()
.filter_map(|r| r.ok())
.collect();
.collect()
} else {
stmt.query_map([], |row| {
Ok(UnfinishedSystem {
system_id: row.get(0)?,
proper_name: row.get(1)?,
star_type: row.get(2)?,
})
})
.unwrap()
.filter_map(|r| r.ok())
.collect()
};
#[derive(Serialize)]
struct UnfinishedResult {
count: usize,
sector: Option<String>,
systems: Vec<UnfinishedSystem>,
}
let result = UnfinishedResult {
count: rows.len(),
sector: sector.map(|s| s.to_string()),
systems: rows,
};
println!("{}", serde_json::to_string_pretty(&result).unwrap());
}
fn cmd_next(conn: &Connection, hop: Option<i32>) {
fn cmd_next(conn: &Connection, hop: Option<i32>, sector: Option<&str>) {
// Find the target hop — either specified or the lowest with unfinished systems
let target_hop: i32 = if let Some(h) = hop {
h
} else {
conn.query_row(
let sql = if sector.is_some() {
"SELECT MIN(g.hop_distance_from_gateway)
FROM star_systems s
JOIN system_gates g ON s.system_id = g.system_id
WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)",
[],
|r| r.get::<_, Option<i32>>(0),
)
WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)
AND s.geographic_sector = ?1"
} else {
"SELECT MIN(g.hop_distance_from_gateway)
FROM star_systems s
JOIN system_gates g ON s.system_id = g.system_id
WHERE s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)"
};
if let Some(s) = sector {
conn.query_row(sql, params![s], |r| r.get::<_, Option<i32>>(0))
} else {
conn.query_row(sql, [], |r| r.get::<_, Option<i32>>(0))
}
.unwrap()
.unwrap_or(-1)
};
@@ -1427,23 +1587,35 @@ fn cmd_next(conn: &Connection, hop: Option<i32>) {
population: Option<i64>,
}
let mut stmt = conn
.prepare(
"SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class,
g.gate_topology, s.geographic_sector,
s.habitable_planet_count, s.inhabited_planet_count,
e.population
FROM star_systems s
JOIN system_gates g ON s.system_id = g.system_id
LEFT JOIN system_economy e ON s.system_id = e.system_id
WHERE g.hop_distance_from_gateway = ?1
AND s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)
ORDER BY s.system_id",
)
.unwrap();
let sql = if sector.is_some() {
"SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class,
g.gate_topology, s.geographic_sector,
s.habitable_planet_count, s.inhabited_planet_count,
e.population
FROM star_systems s
JOIN system_gates g ON s.system_id = g.system_id
LEFT JOIN system_economy e ON s.system_id = e.system_id
WHERE g.hop_distance_from_gateway = ?1
AND s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)
AND s.geographic_sector = ?2
ORDER BY s.system_id"
} else {
"SELECT s.system_id, s.proper_name, s.star_type, s.spectral_class,
g.gate_topology, s.geographic_sector,
s.habitable_planet_count, s.inhabited_planet_count,
e.population
FROM star_systems s
JOIN system_gates g ON s.system_id = g.system_id
LEFT JOIN system_economy e ON s.system_id = e.system_id
WHERE g.hop_distance_from_gateway = ?1
AND s.system_id NOT IN (SELECT DISTINCT system_id FROM bodies)
ORDER BY s.system_id"
};
let systems: Vec<NextSystem> = stmt
.query_map(params![target_hop], |row| {
let mut stmt = conn.prepare(sql).unwrap();
let systems: Vec<NextSystem> = if let Some(s) = sector {
stmt.query_map(params![target_hop, s], |row| {
Ok(NextSystem {
system_id: row.get(0)?,
proper_name: row.get(1)?,
@@ -1458,17 +1630,37 @@ fn cmd_next(conn: &Connection, hop: Option<i32>) {
})
.unwrap()
.filter_map(|r| r.ok())
.collect();
.collect()
} else {
stmt.query_map(params![target_hop], |row| {
Ok(NextSystem {
system_id: row.get(0)?,
proper_name: row.get(1)?,
star_type: row.get(2)?,
spectral_class: row.get(3)?,
gate_topology: row.get(4)?,
geographic_sector: row.get(5)?,
habitable_planet_count: row.get(6)?,
inhabited_planet_count: row.get(7)?,
population: row.get(8)?,
})
})
.unwrap()
.filter_map(|r| r.ok())
.collect()
};
#[derive(Serialize)]
struct NextResult {
hop: i32,
sector: Option<String>,
count: usize,
systems: Vec<NextSystem>,
}
let result = NextResult {
hop: target_hop,
sector: sector.map(|s| s.to_string()),
count: systems.len(),
systems,
};
@@ -1842,8 +2034,14 @@ fn main() {
} => cmd_author(&conn, system_id, outdir, wiki),
Commands::CommitSystem { path } => cmd_commit_system(&conn, path),
Commands::WipeSystem { system_id } => cmd_wipe_system(&conn, system_id),
Commands::Unfinished => cmd_unfinished(&conn),
Commands::Next { hop } => cmd_next(&conn, *hop),
Commands::ListSystems {
sector,
hop,
finished,
unfinished,
} => cmd_list_systems(&conn, sector.as_deref(), *hop, *finished, *unfinished),
Commands::Unfinished { sector } => cmd_unfinished(&conn, sector.as_deref()),
Commands::Next { hop, sector } => cmd_next(&conn, *hop, sector.as_deref()),
Commands::SyncWiki { system_id, wiki } => cmd_sync_wiki(&conn, system_id.as_deref(), wiki),
}
}