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