use crate::cli::responses::*;
use crate::cli::{resolve_db_path, BlastZoneArgs, Cli, OutputFormat};
use crate::output;
use anyhow::Result;
pub fn blast_zone(args: &BlastZoneArgs, cli: &Cli) -> Result<()> {
use crate::cfg::{find_reachable_from_block, load_cfg_from_db, resolve_function_name};
use crate::storage::{compute_path_impact_from_db, get_function_name_db, MirageDb};
use rusqlite::OptionalExtension;
let db_path = resolve_db_path(cli.db.clone())?;
let db = match MirageDb::open(&db_path) {
Ok(db) => db,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::database_not_found(&db_path);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!("Failed to open database: {}", db_path));
output::info("Hint: Run 'magellan watch' to create the database");
std::process::exit(output::EXIT_DATABASE);
}
}
};
if let Some(ref path_id) = args.path_id {
if !db.is_sqlite() {
let msg = "Path-based blast-zone requires SQLite backend. Use block-based analysis with --function and --block-id instead.";
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("UnsupportedBackend", msg, output::E_INVALID_INPUT);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_USAGE);
} else {
output::error(msg);
output::info("retired binary backend backend does not support path caching. Use: mirage blast-zone --function <name> --block-id <id>");
std::process::exit(output::EXIT_USAGE);
}
}
let path_id_trimmed = path_id.trim();
if path_id_trimmed.len() < 10 {
let msg = format!("Invalid path_id format: '{}'", path_id_trimmed);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new("InvalidInput", &msg, output::E_INVALID_INPUT);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_USAGE);
} else {
output::error(&msg);
output::info("Path ID should be a BLAKE3 hash (64 hex characters)");
std::process::exit(output::EXIT_USAGE);
}
}
let (function_id, path_kind): (i64, String) = match db
.conn()?
.query_row(
"SELECT function_id, path_kind FROM cfg_paths WHERE path_id = ?1",
rusqlite::params![path_id_trimmed],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.optional()
{
Ok(Some(data)) => data,
Ok(None) => {
let msg = format!("Path '{}' not found in cache", path_id_trimmed);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("PathNotFound", &msg, output::E_PATH_NOT_FOUND);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_FILE_NOT_FOUND);
} else {
output::error(&msg);
output::info("Hint: Run 'mirage paths' to enumerate paths first");
std::process::exit(output::EXIT_FILE_NOT_FOUND);
}
}
Err(e) => {
let msg = format!("Failed to query path: {}", e);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("DatabaseError", &msg, output::E_DATABASE_NOT_FOUND);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&msg);
std::process::exit(output::EXIT_DATABASE);
}
}
};
if !args.include_errors && path_kind == "error" {
let msg = format!(
"Path '{}' is an error path (use --include-errors to analyze)",
path_id_trimmed
);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("ErrorPathExcluded", &msg, output::E_INVALID_INPUT);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_USAGE);
} else {
output::error(&msg);
output::info("Use --include-errors to include error paths in analysis");
std::process::exit(output::EXIT_USAGE);
}
}
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
let msg = format!("Failed to load CFG for function_id {}", function_id);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new("CgfLoadError", &msg, output::E_CFG_ERROR);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&msg);
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_name = get_function_name_db(&db, function_id)
.unwrap_or_else(|| format!("<function_{}>", function_id));
let max_depth = if args.max_depth == 100 {
None
} else {
Some(args.max_depth)
};
let impact = match compute_path_impact_from_db(db.conn()?, path_id_trimmed, &cfg, max_depth)
{
Ok(impact) => impact,
Err(e) => {
let msg = format!("Failed to compute path impact: {}", e);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new("ImpactError", &msg, output::E_CFG_ERROR);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_ERROR);
} else {
output::error(&msg);
std::process::exit(output::EXIT_ERROR);
}
}
};
let (forward_impact, backward_impact): (
Option<Vec<CallGraphSymbol>>,
Option<Vec<CallGraphSymbol>>,
) = if args.use_call_graph {
use crate::analysis::MagellanBridge;
match MagellanBridge::open(&db_path) {
Ok(bridge) => {
let symbol_id = function_name.as_str();
let (forward, backward) = if args.call_depth > 0 {
let fwd = bridge
.k_hop_callees(symbol_id, args.call_depth)
.map(|symbols| {
symbols
.into_iter()
.map(|ds| CallGraphSymbol {
symbol_id: None,
fqn: Some(ds.info.name.clone()),
file_path: ds.info.file_path.clone().unwrap_or_default(),
kind: ds.info.kind.clone(),
depth: Some(ds.depth),
})
.collect()
})
.ok();
let bwd = bridge
.k_hop_callers(symbol_id, args.call_depth)
.map(|symbols| {
symbols
.into_iter()
.map(|ds| CallGraphSymbol {
symbol_id: None,
fqn: Some(ds.info.name.clone()),
file_path: ds.info.file_path.clone().unwrap_or_default(),
kind: ds.info.kind.clone(),
depth: Some(ds.depth),
})
.collect()
})
.ok();
(fwd, bwd)
} else {
let fwd = bridge
.reachable_symbols(symbol_id)
.map(|symbols| {
symbols
.into_iter()
.map(|s| CallGraphSymbol {
symbol_id: s.symbol_id,
fqn: s.fqn,
file_path: s.file_path,
kind: s.kind,
depth: None,
})
.collect()
})
.ok();
let bwd = bridge
.reverse_reachable_symbols(symbol_id)
.map(|symbols| {
symbols
.into_iter()
.map(|s| CallGraphSymbol {
symbol_id: s.symbol_id,
fqn: s.fqn,
file_path: s.file_path,
kind: s.kind,
depth: None,
})
.collect()
})
.ok();
(fwd, bwd)
};
(forward, backward)
}
Err(e) => {
eprintln!(
"Warning: Could not open Magellan database for call graph analysis: {}",
e
);
eprintln!("Note: --use-call-graph requires a Magellan code graph database");
(None, None)
}
}
} else {
(None, None)
};
match cli.output {
OutputFormat::Human => {
let mut output = String::new();
output.push_str("Path Impact Analysis\n");
output.push('\n');
output.push_str(&format!("Path ID: {}\n", impact.path_id));
output.push_str(&format!("Function: {}\n", function_name));
output.push_str(&format!("Path kind: {}\n", path_kind));
output.push_str(&format!("Path length: {} blocks\n", impact.path_length));
output.push('\n');
if let Some(ref forward) = forward_impact {
if args.call_depth > 0 {
output.push_str(&format!(
"Inter-Procedural Impact (Call Graph, depth-aware, max depth {}):\n",
args.call_depth
));
} else {
output.push_str("Inter-Procedural Impact (Call Graph):\n");
}
output.push_str(&format!(
" Forward Impact: {} functions reached\n",
forward.len()
));
for sym in forward {
let name = sym.fqn.as_deref().unwrap_or(&sym.file_path);
if let Some(d) = sym.depth {
output.push_str(&format!(" [d{}] {}\n", d, name));
} else {
output.push_str(&format!(" - {}\n", name));
}
}
}
if let Some(ref backward) = backward_impact {
if !backward.is_empty() {
if forward_impact.is_none() && args.call_depth > 0 {
output.push_str(&format!(
"Inter-Procedural Impact (Call Graph, depth-aware, max depth {}):\n",
args.call_depth
));
} else if forward_impact.is_none() {
output.push_str("Inter-Procedural Impact (Call Graph):\n");
}
output.push_str(&format!(
" Backward Impact: {} functions can reach this\n",
backward.len()
));
for sym in backward {
let name = sym.fqn.as_deref().unwrap_or(&sym.file_path);
if let Some(d) = sym.depth {
output.push_str(&format!(" [d{}] {}\n", d, name));
} else {
output.push_str(&format!(" - {}\n", name));
}
}
}
}
output.push('\n');
output.push_str("Intra-Procedural Impact (CFG):\n");
output.push_str(&format!(
" Unique blocks affected: {}\n",
impact.impact_count
));
if impact.impact_count > 0 {
output.push_str(&format!(
" Affected blocks: {:?}\n",
impact.unique_blocks_affected
));
} else {
output.push_str(" Affected blocks: (none - path has no downstream impact)\n");
}
if let Some(depth) = max_depth {
output.push_str(&format!(" Max depth: {}\n", depth));
} else {
output.push_str(" Max depth: unlimited\n");
}
let processed = output::apply_token_budget(output, args.tokens);
print!("{}", processed);
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = PathImpactResponse {
path_id: impact.path_id.clone(),
path_length: impact.path_length,
unique_blocks_affected: impact.unique_blocks_affected,
impact_count: impact.impact_count,
forward_impact: forward_impact.clone(),
backward_impact: backward_impact.clone(),
};
let json_str = if matches!(cli.output, OutputFormat::Pretty) {
serde_json::to_string_pretty(&response).unwrap_or_default()
} else {
serde_json::to_string(&response).unwrap_or_default()
};
let processed = output::apply_token_budget(json_str, args.tokens);
let tokens_est = processed.len() / 4;
let truncated = args.tokens.is_some_and(|t| t > 0 && tokens_est > t);
let wrapper = output::JsonResponse::new(response)
.with_tokens(tokens_est)
.with_truncated(truncated);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
} else {
let function_ref = args
.function
.as_ref()
.expect("--function is required for block-based analysis");
let function_id = match resolve_function_name(&db, function_ref) {
Ok(id) => id,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::function_not_found(function_ref);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Function '{}' not found in database",
function_ref
));
output::info(&format!("Hint: {}", output::R_HINT_LIST_FUNCTIONS));
std::process::exit(output::EXIT_DATABASE);
}
}
};
let function_name = get_function_name_db(&db, function_id)
.unwrap_or_else(|| format!("<function_{}>", function_id));
let cfg = match load_cfg_from_db(&db, function_id) {
Ok(cfg) => cfg,
Err(_e) => {
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error = output::JsonError::new(
"CgfLoadError",
&format!("Failed to load CFG for function '{}'", function_ref),
output::E_CFG_ERROR,
);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_DATABASE);
} else {
output::error(&format!(
"Failed to load CFG for function '{}'",
function_ref
));
output::info("The function may be corrupted. Try re-running 'magellan watch'");
std::process::exit(output::EXIT_DATABASE);
}
}
};
let block_id = args.block_id.unwrap_or(0);
let block_exists = cfg.node_indices().any(|n| cfg[n].id == block_id);
if !block_exists {
let valid_blocks: Vec<usize> = cfg.node_indices().map(|n| cfg[n].id).collect();
let msg = format!(
"Block {} not found in function '{}'. Valid blocks: {:?}",
block_id, function_ref, valid_blocks
);
if matches!(cli.output, OutputFormat::Json | OutputFormat::Pretty) {
let error =
output::JsonError::new("BlockNotFound", &msg, output::E_BLOCK_NOT_FOUND);
let wrapper = output::JsonResponse::new(error);
println!("{}", wrapper.to_json());
std::process::exit(output::EXIT_VALIDATION);
} else {
output::error(&msg);
std::process::exit(output::EXIT_VALIDATION);
}
}
let max_depth = if args.max_depth == 100 {
None
} else {
Some(args.max_depth)
};
let impact = find_reachable_from_block(&cfg, block_id, max_depth);
let (forward_impact, backward_impact): (
Option<Vec<CallGraphSymbol>>,
Option<Vec<CallGraphSymbol>>,
) = if args.use_call_graph {
use crate::analysis::MagellanBridge;
match MagellanBridge::open(&db_path) {
Ok(bridge) => {
let symbol_id = function_name.as_str();
let (forward, backward) = if args.call_depth > 0 {
let fwd = bridge
.k_hop_callees(symbol_id, args.call_depth)
.map(|symbols| {
symbols
.into_iter()
.map(|ds| CallGraphSymbol {
symbol_id: None,
fqn: Some(ds.info.name.clone()),
file_path: ds.info.file_path.clone().unwrap_or_default(),
kind: ds.info.kind.clone(),
depth: Some(ds.depth),
})
.collect()
})
.ok();
let bwd = bridge
.k_hop_callers(symbol_id, args.call_depth)
.map(|symbols| {
symbols
.into_iter()
.map(|ds| CallGraphSymbol {
symbol_id: None,
fqn: Some(ds.info.name.clone()),
file_path: ds.info.file_path.clone().unwrap_or_default(),
kind: ds.info.kind.clone(),
depth: Some(ds.depth),
})
.collect()
})
.ok();
(fwd, bwd)
} else {
let fwd = bridge
.reachable_symbols(symbol_id)
.map(|symbols| {
symbols
.into_iter()
.map(|s| CallGraphSymbol {
symbol_id: s.symbol_id,
fqn: s.fqn,
file_path: s.file_path,
kind: s.kind,
depth: None,
})
.collect()
})
.ok();
let bwd = bridge
.reverse_reachable_symbols(symbol_id)
.map(|symbols| {
symbols
.into_iter()
.map(|s| CallGraphSymbol {
symbol_id: s.symbol_id,
fqn: s.fqn,
file_path: s.file_path,
kind: s.kind,
depth: None,
})
.collect()
})
.ok();
(fwd, bwd)
};
(forward, backward)
}
Err(e) => {
eprintln!(
"Warning: Could not open Magellan database for call graph analysis: {}",
e
);
eprintln!("Note: --use-call-graph requires a Magellan code graph database");
(None, None)
}
}
} else {
(None, None)
};
match cli.output {
OutputFormat::Human => {
let mut output = String::new();
output.push_str("Block Impact Analysis (Blast Zone)\n");
output.push('\n');
output.push_str(&format!("Function: {}\n", function_name));
output.push_str(&format!("Source block: {}\n", impact.source_block_id));
output.push('\n');
if let Some(ref forward) = forward_impact {
if args.call_depth > 0 {
output.push_str(&format!(
"Inter-Procedural Impact (Call Graph, depth-aware, max depth {}):\n",
args.call_depth
));
} else {
output.push_str("Inter-Procedural Impact (Call Graph):\n");
}
output.push_str(&format!(
" Forward Impact: {} functions reached\n",
forward.len()
));
for sym in forward {
let name = sym.fqn.as_deref().unwrap_or(&sym.file_path);
if let Some(d) = sym.depth {
output.push_str(&format!(" [d{}] {}\n", d, name));
} else {
output.push_str(&format!(" - {}\n", name));
}
}
}
if let Some(ref backward) = backward_impact {
if !backward.is_empty() {
if forward_impact.is_none() && args.call_depth > 0 {
output.push_str(&format!(
"Inter-Procedural Impact (Call Graph, depth-aware, max depth {}):\n",
args.call_depth
));
} else if forward_impact.is_none() {
output.push_str("Inter-Procedural Impact (Call Graph):\n");
}
output.push_str(&format!(
" Backward Impact: {} functions can reach this\n",
backward.len()
));
for sym in backward {
let name = sym.fqn.as_deref().unwrap_or(&sym.file_path);
if let Some(d) = sym.depth {
output.push_str(&format!(" [d{}] {}\n", d, name));
} else {
output.push_str(&format!(" - {}\n", name));
}
}
}
}
output.push('\n');
output.push_str("Intra-Procedural Impact (CFG):\n");
output.push_str(&format!(" Reachable blocks: {}\n", impact.reachable_count));
if impact.reachable_count > 0 {
output.push_str(&format!(
" Affected blocks: {:?}\n",
impact.reachable_blocks
));
} else {
output.push_str(" Affected blocks: (none - block has no downstream impact)\n");
}
output.push_str(&format!(
" Max depth reached: {}\n",
impact.max_depth_reached
));
output.push_str(&format!(
" Contains cycles: {}\n",
if impact.has_cycles {
"yes (loop detected)"
} else {
"no"
}
));
if let Some(depth) = max_depth {
output.push_str(&format!(" Depth limit: {}\n", depth));
} else {
output.push_str(" Depth limit: unlimited\n");
}
let processed = output::apply_token_budget(output, args.tokens);
print!("{}", processed);
}
OutputFormat::Json | OutputFormat::Pretty => {
let response = BlockImpactResponse {
function: function_name,
block_id: impact.source_block_id,
reachable_blocks: impact.reachable_blocks,
reachable_count: impact.reachable_count,
max_depth: impact.max_depth_reached,
has_cycles: impact.has_cycles,
forward_impact: forward_impact.clone(),
backward_impact: backward_impact.clone(),
};
let json_str = if matches!(cli.output, OutputFormat::Pretty) {
serde_json::to_string_pretty(&response).unwrap_or_default()
} else {
serde_json::to_string(&response).unwrap_or_default()
};
let processed = output::apply_token_budget(json_str, args.tokens);
let tokens_est = processed.len() / 4;
let truncated = args.tokens.is_some_and(|t| t > 0 && tokens_est > t);
let wrapper = output::JsonResponse::new(response)
.with_tokens(tokens_est)
.with_truncated(truncated);
match cli.output {
OutputFormat::Json => println!("{}", wrapper.to_json()),
OutputFormat::Pretty => println!("{}", wrapper.to_pretty_json()),
_ => unreachable!(),
}
}
}
}
Ok(())
}