use anyhow::Result;
use colored::Colorize;
use crate::exit_codes::*;
#[derive(Debug)]
pub struct ImpactArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub file_path: String,
pub max_depth: i32,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct LibraryArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub library_name: String,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct DepsArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub file_path: String,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct CriticalArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub limit: i32,
pub sort_by: String,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct FunctionImpactArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub function: String,
pub max_depth: i32,
pub json: bool,
pub verbose: bool,
}
#[derive(Debug)]
pub struct StatsArgs {
pub session_id: Option<String>,
pub workspace_path: Option<String>,
pub json: bool,
pub verbose: bool,
}
pub async fn execute_impact(args: ImpactArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Analyzing impact of: {}", "→".cyan(), args.file_path);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let impact = unfault_analysis::graph::traversal::get_impact(
&graph,
&args.file_path,
args.max_depth as usize,
);
if impact.affected_files.is_empty() {
eprintln!(
"{} No downstream dependencies found for '{}'",
"ℹ".cyan(),
args.file_path
);
return Ok(EXIT_SUCCESS);
}
if args.json {
println!("{}", serde_json::to_string_pretty(&impact)?);
} else {
println!(
"\n{} Impact analysis for {}",
"📊".bright_blue(),
args.file_path.bright_blue()
);
println!(
" {} {} file(s) affected:\n",
"→".cyan(),
impact.affected_files.len()
);
for file in &impact.affected_files {
println!(" {}", file);
}
println!();
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_library(args: LibraryArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Finding files using: {}", "→".cyan(), args.library_name);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let deps = unfault_analysis::graph::traversal::get_dependencies(&graph, &args.library_name);
if args.json {
println!("{}", serde_json::to_string_pretty(&deps)?);
} else {
println!(
"\n{} Files using '{}':",
"📦".bright_blue(),
args.library_name.bright_blue()
);
if deps.library_users.is_empty() && deps.dependencies.is_empty() {
println!(" No files found using '{}'", args.library_name);
} else {
for file in &deps.library_users {
println!(" {}", file);
}
for file in &deps.dependencies {
println!(" {}", file);
}
}
println!();
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_deps(args: DepsArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Finding dependencies of: {}", "→".cyan(), args.file_path);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let deps = unfault_analysis::graph::traversal::get_dependencies(&graph, &args.file_path);
if args.json {
println!("{}", serde_json::to_string_pretty(&deps)?);
} else {
println!(
"\n{} Dependencies of {}",
"📦".bright_blue(),
args.file_path.bright_blue()
);
if !deps.dependencies.is_empty() {
println!(" Internal modules:");
for dep in &deps.dependencies {
println!(" {}", dep);
}
}
if !deps.library_users.is_empty() {
println!(" External libraries:");
for lib in &deps.library_users {
println!(" {}", lib);
}
}
if deps.dependencies.is_empty() && deps.library_users.is_empty() {
println!(" No dependencies found for '{}'", args.file_path);
}
println!();
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_critical(args: CriticalArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!(
"{} Finding top {} critical files (sorted by {})",
"→".cyan(),
args.limit,
args.sort_by
);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
if args.sort_by == "importance_score" {
let ranked = unfault_analysis::sre::ranker::top_n(&graph, &[], args.limit as usize);
if args.json {
println!("{}", serde_json::to_string_pretty(&ranked)?);
} else {
println!(
"\n{} Most critical files (composite importance score):\n",
"📊".bright_blue()
);
if ranked.is_empty() {
println!(" No files found in graph.");
} else {
for (i, rf) in ranked.iter().enumerate() {
println!(
" {}. {} (score: {:.2} centrality: {:.2} lib-risk: {:.2} debt: {:.2})",
i + 1,
rf.file_path.bright_blue(),
rf.importance_score,
rf.centrality_score,
rf.library_risk_score,
rf.finding_density_score,
);
}
}
println!();
}
} else {
let centrality =
unfault_analysis::graph::traversal::get_centrality(&graph, args.limit as usize);
if args.json {
println!("{}", serde_json::to_string_pretty(¢rality)?);
} else {
println!(
"\n{} Most critical files (by import count):\n",
"📊".bright_blue()
);
if centrality.central_files.is_empty() {
println!(" No import relationships found.");
} else {
for (i, (path, score)) in centrality.central_files.iter().enumerate() {
println!(
" {}. {} (imported {} times)",
i + 1,
path.bright_blue(),
(*score as i32).to_string().yellow()
);
}
}
println!();
}
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_stats(args: StatsArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!("{} Building graph statistics...", "→".cyan());
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let overview = unfault_analysis::graph::traversal::workspace_overview(&graph);
if args.json {
println!("{}", serde_json::to_string_pretty(&overview)?);
} else {
println!("\n{} Graph Statistics\n", "📊".bright_blue());
println!(" Files: {}", overview.file_count.to_string().yellow());
println!(
" Functions: {}",
overview.function_count.to_string().yellow()
);
println!(" Languages: {}", overview.languages.join(", ").cyan());
if !overview.frameworks.is_empty() {
println!(" Frameworks: {}", overview.frameworks.join(", ").cyan());
}
println!(
" Nodes: {}",
graph.graph.node_count().to_string().yellow()
);
println!(
" Edges: {}",
graph.graph.edge_count().to_string().yellow()
);
if !overview.entrypoints.is_empty() {
println!("\n Entrypoints:");
for ep in &overview.entrypoints {
println!(" {}", ep);
}
}
println!();
}
Ok(EXIT_SUCCESS)
}
pub async fn execute_function_impact(args: FunctionImpactArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
let (_file_path, function_name) = match args.function.split_once(':') {
Some((file, func)) => (file.to_string(), func.to_string()),
None => {
eprintln!(
"{} Function must be in format file:function (e.g., main.py:process_user)",
"Error:".red().bold()
);
return Ok(EXIT_ERROR);
}
};
if args.verbose {
eprintln!("{} Analyzing call flow of: {}", "→".cyan(), args.function);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let flow = unfault_analysis::graph::traversal::extract_flow(
&graph,
&function_name,
args.max_depth as usize,
);
if args.json {
println!("{}", serde_json::to_string_pretty(&flow)?);
} else {
println!();
println!(
"{} {} {}",
"🔗".cyan(),
"Function Call Graph:".bold(),
function_name.bright_white()
);
println!();
if flow.paths.is_empty() {
println!(" {} No call paths found from this function.", "ℹ".blue());
println!();
return Ok(EXIT_SUCCESS);
}
println!(
" {} Found {} call path(s)",
"→".cyan(),
flow.paths.len().to_string().bold()
);
println!();
for (i, path) in flow.paths.iter().enumerate() {
println!(" Path {}:", i + 1);
for node in path {
let indent = " ".repeat(node.depth + 2);
let file_info = node
.file_path
.as_deref()
.map(|p| format!(" ({})", p))
.unwrap_or_default();
println!(
"{}{} {}{}",
indent,
"→".cyan(),
node.name.bright_white(),
file_info.dimmed()
);
}
println!();
}
}
Ok(EXIT_SUCCESS)
}
#[derive(Debug)]
pub struct CallersArgs {
pub workspace_path: Option<String>,
pub function: String,
pub max_depth: i32,
pub json: bool,
pub verbose: bool,
}
pub async fn execute_callers(args: CallersArgs) -> Result<i32> {
let workspace_path = match &args.workspace_path {
Some(p) => std::path::PathBuf::from(p),
None => std::env::current_dir()?,
};
let (file_hint, function_name) = match args.function.split_once(':') {
Some((file, func)) => (Some(file.to_string()), func.to_string()),
None => (None, args.function.clone()),
};
if args.verbose {
eprintln!("{} Tracing callers of: {}", "→".cyan(), args.function);
}
let graph = match crate::local_graph::build_analysis_graph(&workspace_path, args.verbose) {
Ok(g) => g,
Err(e) => {
eprintln!(
"{} Failed to build code graph: {}",
"Error:".red().bold(),
e
);
return Ok(EXIT_ERROR);
}
};
let ctx = if let Some(ref hint) = file_hint {
unfault_analysis::graph::traversal::get_callers_in_file(
&graph,
&function_name,
hint,
args.max_depth as usize,
)
} else {
unfault_analysis::graph::traversal::get_callers(
&graph,
&function_name,
args.max_depth as usize,
)
};
if args.json {
println!("{}", serde_json::to_string_pretty(&ctx)?);
return Ok(EXIT_SUCCESS);
}
println!();
if ctx.callers.is_empty() && ctx.routes.is_empty() {
let not_in_graph = ctx.target_file.is_none();
if not_in_graph {
println!(
" {} '{}' was not found in the code graph.",
"ℹ".cyan(),
function_name
);
} else {
println!(
" {} '{}' is in the graph ({}) but no call edges were resolved.",
"ℹ".cyan(),
function_name,
ctx.target_file.as_deref().unwrap_or("").dimmed()
);
println!(
" {} Cross-file calls are not yet tracked — try targeting a route handler",
" ".dimmed()
);
println!(" {} in the same file directly.", " ".dimmed());
}
let suggestions = unfault_analysis::graph::traversal::suggest_callers_candidates(
&graph,
&function_name,
ctx.target_file.as_deref(),
);
if !suggestions.is_empty() {
println!();
let has_handlers = suggestions.iter().any(|s| s.http_method.is_some());
if not_in_graph {
println!(" Did you mean one of these?");
} else if has_handlers {
let location = if suggestions.iter().any(|s| s.reason == "same_file_handler") {
"same file"
} else {
"same module"
};
println!(
" Route handlers in the {} — likely entry points for this function:",
location
);
} else {
println!(" Most-called functions in the workspace:");
}
println!();
for s in &suggestions {
if let (Some(method), Some(path)) = (&s.http_method, &s.http_path) {
let method_colored = match method.as_str() {
"GET" => method.bright_green(),
"POST" => method.bright_yellow(),
"PUT" | "PATCH" => method.bright_cyan(),
"DELETE" => method.bright_red(),
_ => method.normal(),
};
println!(
" {} {} {} {}",
method_colored,
path,
"→".dimmed(),
s.name.bright_white()
);
println!(" {}", s.file.dimmed());
} else {
println!(" {}", s.name.bright_white());
if !s.file.is_empty() {
println!(" {}", s.file.dimmed());
}
}
println!();
}
if not_in_graph {
println!(
" Use {} to target a specific file:",
"file.py:function_name".bold()
);
if let Some(first) = suggestions.first() {
println!(" unfault graph callers {}:{}", first.file, first.name);
}
} else if let Some(first) = suggestions.iter().find(|s| s.http_method.is_some()) {
println!(
" Run {} or {} on one of the handlers above:",
"graph callers".bold(),
"fault".bold()
);
println!(" unfault graph callers {}:{}", first.file, first.name);
println!(" unfault fault {}:{}", first.file, first.name);
}
}
println!();
return Ok(EXIT_SUCCESS);
}
println!(
"{} {}",
"Call path to".bold(),
function_name.bright_white().bold()
);
if let Some(ref f) = ctx.target_file {
println!(" {}", f.dimmed());
}
println!();
for route in &ctx.routes {
let method_colored = match route.method.as_str() {
"GET" => route.method.bright_green(),
"POST" => route.method.bright_yellow(),
"PUT" | "PATCH" => route.method.bright_cyan(),
"DELETE" => route.method.bright_red(),
_ => route.method.normal(),
};
println!(" {} {}", method_colored, route.path.bold());
}
if !ctx.routes.is_empty() {
println!();
}
use std::collections::HashMap;
use unfault_analysis::types::graph_query::CallerInfo;
let max_depth = ctx.callers.iter().map(|c| c.depth).max().unwrap_or(0);
let mut by_depth: HashMap<usize, Vec<&CallerInfo>> = HashMap::new();
for c in &ctx.callers {
by_depth.entry(c.depth).or_insert_with(Vec::new).push(c);
}
let _top_callers: &[&CallerInfo] = by_depth
.get(&max_depth)
.map(|v| v.as_slice())
.unwrap_or(&[]);
fn render_level(
depth: usize,
by_depth: &HashMap<usize, Vec<&CallerInfo>>,
target: &str,
target_file: Option<&str>,
prefix: &str,
) {
let nodes = by_depth.get(&depth).map(|v| v.as_slice()).unwrap_or(&[]);
for (i, node) in nodes.iter().enumerate() {
let is_last_at_level = i == nodes.len() - 1;
let connector = if is_last_at_level { "└─" } else { "├─" };
let file_info = node
.file
.as_deref()
.map(|p| format!(" ({})", p))
.unwrap_or_default();
println!(
"{}{} {}{}",
prefix,
connector.dimmed(),
node.name.bright_white(),
file_info.dimmed()
);
let child_prefix = if is_last_at_level {
format!("{} ", prefix)
} else {
format!("{}│ ", prefix)
};
if depth > 1 {
render_level(depth - 1, by_depth, target, target_file, &child_prefix);
} else {
let target_file_info = target_file.map(|p| format!(" ({})", p)).unwrap_or_default();
println!(
"{}└─ {}{} {}",
child_prefix,
target.bright_blue().bold(),
target_file_info.dimmed(),
"← you are here".cyan().dimmed()
);
}
}
}
if max_depth == 0 {
let target_file_info = ctx
.target_file
.as_deref()
.map(|p| format!(" ({})", p))
.unwrap_or_default();
println!(
" └─ {}{} {}",
ctx.target.bright_blue().bold(),
target_file_info.dimmed(),
"← you are here".cyan().dimmed()
);
} else {
render_level(
max_depth,
&by_depth,
&ctx.target,
ctx.target_file.as_deref(),
" ",
);
}
println!();
Ok(EXIT_SUCCESS)
}
#[derive(Debug)]
pub struct DumpArgs {
pub workspace_path: Option<String>,
pub calls_only: bool,
pub file: Option<String>,
pub verbose: bool,
}
pub fn execute_dump(args: DumpArgs) -> Result<i32> {
use crate::session::graph_builder::build_local_graph;
let workspace_path = match &args.workspace_path {
Some(path) => std::path::PathBuf::from(path),
None => std::env::current_dir()?,
};
if args.verbose {
eprintln!(
"{} Building local code graph for: {}",
"→".cyan(),
workspace_path.display()
);
}
let graph = build_local_graph(&workspace_path, None, args.verbose)?;
if args.verbose {
eprintln!(
"{} Graph built: {} files, {} functions, {} call edges",
"✓".green(),
graph.files.len(),
graph.functions.len(),
graph.calls.len()
);
eprintln!();
}
if args.calls_only {
let calls: Vec<_> = if let Some(ref file_filter) = args.file {
graph
.calls
.iter()
.filter(|c| c.caller_file.contains(file_filter))
.collect()
} else {
graph.calls.iter().collect()
};
println!("{}", serde_json::to_string_pretty(&calls)?);
} else if let Some(ref file_filter) = args.file {
#[derive(serde::Serialize)]
struct FileGraph {
file: Option<crate::session::graph_builder::FileNode>,
functions: Vec<crate::session::graph_builder::FunctionNode>,
outgoing_calls: Vec<crate::session::graph_builder::CallEdge>,
incoming_calls: Vec<crate::session::graph_builder::CallEdge>,
imports: Vec<crate::session::graph_builder::ImportEdge>,
}
let file_graph = FileGraph {
file: graph
.files
.iter()
.find(|f| f.path.contains(file_filter))
.cloned(),
functions: graph
.functions
.iter()
.filter(|f| f.file_path.contains(file_filter))
.cloned()
.collect(),
outgoing_calls: graph
.calls
.iter()
.filter(|c| c.caller_file.contains(file_filter))
.cloned()
.collect(),
incoming_calls: graph
.calls
.iter()
.filter(|c| {
graph
.functions
.iter()
.any(|f| f.qualified_name == c.callee && f.file_path.contains(file_filter))
})
.cloned()
.collect(),
imports: graph
.imports
.iter()
.filter(|i| i.from_file.contains(file_filter) || i.to_file.contains(file_filter))
.cloned()
.collect(),
};
println!("{}", serde_json::to_string_pretty(&file_graph)?);
} else {
println!("{}", serde_json::to_string_pretty(&graph)?);
}
Ok(EXIT_SUCCESS)
}