Skip to main content

tldr_cli/commands/
hubs.rs

1//! Hubs command - Detect high-centrality hub functions
2//!
3//! Identifies "hub" functions that are change amplifiers - modifications to them
4//! affect many other parts of the codebase. Uses graph centrality algorithms
5//! to quantify risk.
6//!
7//! # Algorithms
8//!
9//! - `in_degree`: How many functions call this one (dependencies)
10//! - `out_degree`: How many functions this one calls (complexity)
11//! - `pagerank`: Recursive importance based on caller importance
12//! - `betweenness`: How often this lies on shortest paths (bottleneck)
13//!
14//! # Premortem Mitigations
15//! - T14: CLI registration follows existing pattern
16//! - T16: Small graph (<10 nodes) messaging
17//! - T18: Text formatting follows spec style guide
18
19use std::path::PathBuf;
20
21use anyhow::Result;
22use clap::{Args, ValueEnum};
23
24use tldr_core::analysis::hubs::{compute_hub_report, HubAlgorithm};
25use tldr_core::callgraph::{build_forward_graph, build_reverse_graph, collect_nodes};
26use tldr_core::{build_project_call_graph, Language};
27
28use crate::output::{format_hubs_text, OutputFormat, OutputWriter};
29
30/// Algorithm selection for CLI (mirrors HubAlgorithm)
31#[derive(Debug, Clone, Copy, Default, ValueEnum)]
32pub enum AlgorithmArg {
33    /// All algorithms: in_degree, out_degree, pagerank, betweenness
34    #[default]
35    All,
36    /// In-degree only (fast)
37    Indegree,
38    /// Out-degree only (fast)
39    Outdegree,
40    /// PageRank only
41    Pagerank,
42    /// Betweenness only (slow for large graphs)
43    Betweenness,
44}
45
46impl From<AlgorithmArg> for HubAlgorithm {
47    fn from(arg: AlgorithmArg) -> Self {
48        match arg {
49            AlgorithmArg::All => HubAlgorithm::All,
50            AlgorithmArg::Indegree => HubAlgorithm::InDegree,
51            AlgorithmArg::Outdegree => HubAlgorithm::OutDegree,
52            AlgorithmArg::Pagerank => HubAlgorithm::PageRank,
53            AlgorithmArg::Betweenness => HubAlgorithm::Betweenness,
54        }
55    }
56}
57
58/// Detect hub functions using centrality analysis
59#[derive(Debug, Args)]
60pub struct HubsArgs {
61    /// Project root directory (default: current directory)
62    #[arg(default_value = ".")]
63    pub path: PathBuf,
64
65    /// Number of top hubs to return
66    #[arg(long, default_value = "10")]
67    pub top: usize,
68
69    /// Centrality algorithm to use
70    #[arg(long, value_enum, default_value = "all")]
71    pub algorithm: AlgorithmArg,
72
73    /// Minimum composite score threshold (0.0-1.0)
74    #[arg(long)]
75    pub threshold: Option<f64>,
76
77    /// Programming language (auto-detect if not specified)
78    #[arg(long, short = 'l')]
79    pub lang: Option<Language>,
80}
81
82impl HubsArgs {
83    /// Run the hubs command
84    pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
85        let writer = OutputWriter::new(format, quiet);
86
87        // Validate path exists
88        if !self.path.exists() {
89            anyhow::bail!("Path not found: {}", self.path.display());
90        }
91
92        // Validate threshold if provided
93        if let Some(thresh) = self.threshold {
94            if !(0.0..=1.0).contains(&thresh) {
95                anyhow::bail!("Threshold must be between 0.0 and 1.0, got {}", thresh);
96            }
97        }
98
99        // Determine language (auto-detect from directory, default to Python)
100        let language = self
101            .lang
102            .unwrap_or_else(|| Language::from_directory(&self.path).unwrap_or(Language::Python));
103
104        writer.progress(&format!(
105            "Building call graph for {} ({:?})...",
106            self.path.display(),
107            language
108        ));
109
110        // Build call graph
111        let graph = build_project_call_graph(&self.path, language, None, true)?;
112
113        writer.progress("Computing hub centrality metrics...");
114
115        // Build graph representations
116        let forward = build_forward_graph(&graph);
117        let reverse = build_reverse_graph(&graph);
118        let nodes = collect_nodes(&graph);
119
120        // Compute hub report
121        let report = compute_hub_report(
122            &nodes,
123            &forward,
124            &reverse,
125            self.algorithm.into(),
126            self.top,
127            self.threshold,
128        );
129
130        // Output based on format
131        if writer.is_text() {
132            let text = format_hubs_text(&report);
133            writer.write_text(&text)?;
134        } else {
135            writer.write(&report)?;
136        }
137
138        Ok(())
139    }
140}