From b4dcfceef6ca57ea7a95380a69b4368282fc05bf Mon Sep 17 00:00:00 2001 From: hermes Date: Sat, 3 Oct 2026 16:21:35 +0000 Subject: [PATCH] fix(cli): keep total-failure JSON errors parseable Remove obsolete unsupported-import diagnostics and defer comparison warnings until partial success. Preserve per-symbol diagnostics and quiet behavior when results remain available. --- docs/ARCHITECTURE.md | 1 + src/cli/ownership.rs | 30 ++++++++---------------------- src/cli/stocks.rs | 19 +++++++++++-------- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c02e846..bbfc9f2 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -137,6 +137,7 @@ CLI flags > environment variables > config file > defaults - Table mode: human-readable error on stderr - JSON mode: `{"error": true, "code": "...", "message": "..."}` - Exit code 0 on success, non-zero on failure +- A totally failed `stocks compare` emits one structured error in JSON mode; per-symbol warnings are retained for partial success, not printed ahead of a total-failure error. ## CLI Output and Offline Cache - `version`, `cache info`, `cache clear`, and `ownership resolve map|merge` emit diff --git a/src/cli/ownership.rs b/src/cli/ownership.rs index a04cb7c..cf4f369 100644 --- a/src/cli/ownership.rs +++ b/src/cli/ownership.rs @@ -18,7 +18,6 @@ use crate::ownership::types::{ ChangeType, FlowSignal, HolderRow, KseiHolding, OwnershipRelease, OwnershipSource, }; use crate::ownership::{archive, db, entities, graph, parser, remote, search, snapshot, xlsx}; -use crate::runtime; #[derive(Debug, Args)] #[command( @@ -786,28 +785,15 @@ fn handle_import(args: &ImportArgs, config: &IdxConfig) -> Result<(), IdxError> )); } - if let Some(symbols) = &args.fetch_bing { - let clean: Vec = symbols + if args.fetch_bing.as_ref().is_some_and(|symbols| { + symbols .iter() - .flat_map(|v| v.split(',')) - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(|s| s.to_uppercase()) - .collect(); - - if !clean.is_empty() { - runtime::info(format!( - "--fetch-bing requested for {} symbol(s), implementation deferred for Sprint 6", - clean.len() - )); - for symbol in &clean { - runtime::info(format!(" - {symbol}")); - } - - return Err(IdxError::Unsupported( - "--fetch-bing import is not implemented yet".to_string(), - )); - } + .flat_map(|value| value.split(',')) + .any(|value| !value.trim().is_empty()) + }) { + return Err(IdxError::Unsupported( + "--fetch-bing import is not implemented yet".to_string(), + )); } let Some(import_input) = resolve_import_input(args)? else { diff --git a/src/cli/stocks.rs b/src/cli/stocks.rs index 1d15d5f..676e327 100644 --- a/src/cli/stocks.rs +++ b/src/cli/stocks.rs @@ -647,7 +647,7 @@ pub fn handle( } StocksSubcommand::Compare { symbols } => { let mut reports: Vec = Vec::new(); - let mut last_error = None; + let mut failures = Vec::new(); for sym in symbols.iter().flat_map(|s| s.split(',')) { let resolved = crate::api::resolve_symbol(sym, &config.exchange)?; @@ -664,21 +664,24 @@ pub fn handle( analyze_fundamental, ) { Ok(report) => reports.push(report), - Err(err) => { - runtime::warn(format!( - "failed to fetch fundamentals for {resolved}: {err}" - )); - last_error = Some(err); - } + Err(err) => failures.push((resolved, err)), } } if reports.is_empty() { - return Err(last_error.unwrap_or_else(|| { + return Err(failures.pop().map(|(_, err)| err).unwrap_or_else(|| { IdxError::CacheMiss("fundamental/no symbols could be compared".to_string()) })); } + // Partial success needs diagnostics; total failure must leave stderr + // available for the single structured error emitted by the caller. + for (resolved, err) in failures { + runtime::warn(format!( + "failed to fetch fundamentals for {resolved}: {err}" + )); + } + render_compare(&reports, &config.output, config.no_color) } }