diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index a9c50b6..9c39769 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -2740,6 +2740,8 @@ struct DataCmd { enum DataCommand { /// Report a symbol's monthly file-index coverage (span + interior gaps). Coverage(DataCoverageCmd), + /// List the archive's known symbols, sorted, one per line. + List, } #[derive(Args)] @@ -3674,9 +3676,37 @@ fn dispatch_nodes_new(a: NodesNewCmd) { fn dispatch_data(cmd: DataCmd, env: &project::Env) { match cmd.command { DataCommand::Coverage(a) => dispatch_data_coverage(a, env), + DataCommand::List => dispatch_data_list(env), } } +/// `aura data list` (#264 cut 2): print the archive's known symbols, sorted +/// ascending, one per line — the discovery step before `aura data coverage` +/// or scoping a campaign's instrument matrix. Resolves the archive root +/// through the normal project path ([`dispatch_data_coverage`]'s +/// `Env::data_path`), then reuses `DataServer`'s own symbol index +/// (`DataServer::symbols()`, already sorted) rather than re-deriving a +/// directory scan. An empty or absent archive is informational absence, not a +/// fault: a `no symbols` prose line, exit 0 (the verb corpus's established +/// empty-result register — cf. `runs family`'s unknown-id empty exit 0). +fn dispatch_data_list(env: &project::Env) { + let data_path = std::path::PathBuf::from(env.data_path()); + let server = aura_ingest::DataServer::new(&data_path); + for line in data_list_report(&server.symbols()) { + println!("{line}"); + } +} + +/// Pure: render `symbols` (already sorted by `DataServer::symbols()`) as one +/// line per symbol, or a single `no symbols` line when the archive is empty +/// or absent — informational absence, never a fault (#264). +fn data_list_report(symbols: &[std::sync::Arc]) -> Vec { + if symbols.is_empty() { + return vec!["no symbols".to_string()]; + } + symbols.iter().map(|s| s.to_string()).collect() +} + /// `aura data coverage ` (#264 cut 1): print the archive's month /// coverage for `SYMBOL` — the Copper failure mode (files present at both /// ends of a window while an interior month is missing, so a first/last-bounds @@ -4203,6 +4233,26 @@ mod tests { ); } + /// An empty (or absent) archive is informational absence, not a fault + /// (#264): `data_list_report` returns the single `no symbols` prose line + /// rather than an empty `Vec`, so the caller's exit-0 println always has + /// something to say. + #[test] + fn data_list_report_of_an_empty_archive_is_a_no_symbols_line() { + let symbols: Vec> = vec![]; + assert_eq!(data_list_report(&symbols), vec!["no symbols".to_string()]); + } + + /// A non-empty archive renders one line per symbol, in the order handed + /// in (`DataServer::symbols()` already sorts) — no `no symbols` line + /// mixed in (#264). + #[test] + fn data_list_report_of_a_populated_archive_is_one_line_per_symbol() { + let symbols: Vec> = + vec![std::sync::Arc::from("SYMA"), std::sync::Arc::from("SYMB")]; + assert_eq!(data_list_report(&symbols), vec!["SYMA".to_string(), "SYMB".to_string()]); + } + /// Disjoint archives (no shared instant across all listed symbols) refuse /// rather than silently pooling a floor over different periods per /// instrument (#213); the error names each symbol next to its own span,