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
//! Score command — rank symbols by interestingness
//!
//! Computes scores for symbols based on static, CFG, and temporal features.
//! Outputs ranked candidates for optimization, review, or vulnerability analysis.
//!
//! Usage:
//! magellan score --db <db> — score all symbols
//! magellan score --db <db> --top 10 — show top 10 candidates
//! magellan score --db <db> --min-churn 5 — filter by minimum churn
//! magellan score --db <db> --output json — JSON output
use anyhow::{Context, Result};
use magellan::graph::scorer::{ScoreFilters, ScorerOps};
/// Run the score command
///
/// # Arguments
/// * `db` - Database path
/// * `top` - Show top N candidates
/// * `min_score` - Filter by minimum score
/// * `min_churn` - Filter by minimum churn count
/// * `min_complexity` - Filter by minimum complexity
/// * `min_lifetime` - Filter by minimum lifetime
/// * `output_format` - Output format (human/json/pretty)
pub fn run_score(
db: &std::path::Path,
top: Option<usize>,
min_score: Option<f64>,
min_churn: Option<i64>,
min_complexity: Option<i64>,
min_lifetime: Option<i64>,
output_format: magellan::OutputFormat,
) -> Result<()> {
// If no filters/top specified, default to scoring all
let should_score_all = top.is_none()
&& min_score.is_none()
&& min_churn.is_none()
&& min_complexity.is_none()
&& min_lifetime.is_none();
let mut ops = ScorerOps::from_db_path(db)
.with_context(|| format!("Failed to open scorer operations for {}", db.display()))?;
if should_score_all {
// Score all symbols
let summary = ops.score_all().map_err(|e| {
let mut msg = format!("Detailed error: {}", e);
for cause in e.chain() {
msg.push_str(&format!("\n caused by: {}", cause));
}
eprintln!("{}", msg);
anyhow::anyhow!("Failed to score all symbols")
})?;
match output_format {
magellan::OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&summary)?);
}
magellan::OutputFormat::Human | magellan::OutputFormat::Pretty => {
println!(
"Scored {} symbols in {:?}",
summary.symbols_scored, summary.duration
);
println!("Scorer version: {}", summary.scorer_version);
println!("Run ID: {}", summary.id);
// Show top 10
if let Ok(top) = ops.top_candidates(10) {
println!("\nTop 10 candidates:");
for (i, candidate) in top.iter().enumerate() {
println!(
" {}. {} — score: {:.2}, churn: {}, complexity: {}",
i + 1,
candidate.stable_id,
candidate.score,
candidate.feature_churn_count,
candidate.feature_complexity
);
}
}
}
}
} else {
// Query with filters
let filters = ScoreFilters {
min_score,
min_churn,
min_complexity,
min_lifetime,
limit: top,
};
let candidates = ops
.query_candidates(&filters)
.with_context(|| "Failed to query candidates")?;
match output_format {
magellan::OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&candidates)?);
}
magellan::OutputFormat::Human | magellan::OutputFormat::Pretty => {
let limit = filters.limit.unwrap_or(candidates.len());
println!("Showing {} of {} candidates:", limit, candidates.len());
println!();
for (i, candidate) in candidates.iter().enumerate() {
println!(
"{}. {} — score: {:.2}",
i + 1,
candidate.stable_id,
candidate.score
);
println!(
" LOC: {}, fan_in: {}, fan_out: {}, complexity: {}",
candidate.feature_loc,
candidate.feature_fan_in,
candidate.feature_fan_out,
candidate.feature_complexity
);
println!(
" CFG blocks: {}, CFG edges: {}, conditional_density: {:.2}",
candidate.feature_cfg_block_count,
candidate.feature_cfg_edge_count,
candidate.feature_conditional_density
);
println!(
" Lifetime: {}, churn: {}",
candidate.feature_lifetime, candidate.feature_churn_count
);
println!();
}
}
}
}
Ok(())
}