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