use crate::{analyze::filter_excluded, collect_files};
pub fn cmd_calibrate(paths: &[String], apply: bool) {
let cwd = std::env::current_dir().unwrap_or_default();
let root_config = crate::load_config(&cwd);
let files = filter_excluded(collect_files(paths), &root_config.exclude, &cwd);
let mut cache = crate::open_project_cache(&cwd);
let (mut lines, mut cx, mut cog) = collect_stats(&files, &cwd, &mut cache);
cache.flush();
if lines.is_empty() {
println!("No functions found.");
return;
}
lines.sort();
cx.sort();
cog.sort();
print_stats(&lines, &cx, &cog, files.len());
if apply {
write_calibration(&cwd, &lines, &cx, &cog, files.len());
} else {
println!("\nRun `cha calibrate --apply` to save thresholds to .cha/calibration.toml");
println!("(cha analyze will auto-apply them as defaults)");
}
}
fn collect_stats(
files: &[std::path::PathBuf],
cwd: &std::path::Path,
cache: &mut cha_core::ProjectCache,
) -> (Vec<usize>, Vec<usize>, Vec<usize>) {
let (mut lines, mut cx, mut cog) = (Vec::new(), Vec::new(), Vec::new());
for path in files {
let Some((_, model)) = crate::cached_parse(path, cache, cwd) else {
continue;
};
for f in &model.functions {
lines.push(f.line_count);
cx.push(f.complexity);
cog.push(f.cognitive_complexity);
}
}
(lines, cx, cog)
}
fn pct(v: &[usize], p: usize) -> usize {
v[v.len() * p / 100]
}
fn print_stats(lines: &[usize], cx: &[usize], cog: &[usize], file_count: usize) {
println!(
"Analyzed {} functions across {file_count} files.\n",
lines.len()
);
println!(
"{:<25} {:>10} {:>10}",
"Metric", "Warning(P90)", "Error(P95)"
);
println!("{}", "─".repeat(48));
println!(
"{:<25} {:>10} {:>10}",
"long_method",
pct(lines, 90),
pct(lines, 95)
);
println!(
"{:<25} {:>10} {:>10}",
"high_complexity",
pct(cx, 90),
pct(cx, 95)
);
println!(
"{:<25} {:>10} {:>10}",
"cognitive_complexity",
pct(cog, 90),
pct(cog, 95)
);
}
fn write_calibration(
cwd: &std::path::Path,
lines: &[usize],
cx: &[usize],
cog: &[usize],
file_count: usize,
) {
let dir = cwd.join(".cha");
std::fs::create_dir_all(&dir).ok();
let content = format!(
"# Auto-generated by `cha calibrate`. Override in .cha.toml.\n\n\
[stats]\n\
functions = {}\n\
files = {file_count}\n\n\
[thresholds]\n\
max_function_lines = {}\n\
max_complexity = {}\n\
max_cognitive_complexity = {}\n\n\
[distribution]\n\
lines_p50 = {}\n\
lines_p75 = {}\n\
lines_p90 = {}\n\
lines_p95 = {}\n\
complexity_p50 = {}\n\
complexity_p75 = {}\n\
complexity_p90 = {}\n\
complexity_p95 = {}\n\
cognitive_p50 = {}\n\
cognitive_p75 = {}\n\
cognitive_p90 = {}\n\
cognitive_p95 = {}\n",
lines.len(),
pct(lines, 90),
pct(cx, 90),
pct(cog, 90),
pct(lines, 50),
pct(lines, 75),
pct(lines, 90),
pct(lines, 95),
pct(cx, 50),
pct(cx, 75),
pct(cx, 90),
pct(cx, 95),
pct(cog, 50),
pct(cog, 75),
pct(cog, 90),
pct(cog, 95),
);
let path = dir.join("calibration.toml");
std::fs::write(&path, content).unwrap();
println!("\n✓ Saved to {}", path.display());
println!(" cha analyze will now use these as default thresholds.");
println!(" Override in .cha.toml for custom values.");
}
pub fn load_calibration(cwd: &std::path::Path) -> Option<(usize, usize, usize)> {
let content = std::fs::read_to_string(cwd.join(".cha/calibration.toml")).ok()?;
let get = |key: &str| -> Option<usize> {
for line in content.lines() {
let line = line.trim();
if line.starts_with(key) {
return line.split('=').nth(1)?.trim().parse().ok();
}
}
None
};
Some((
get("max_function_lines")?,
get("max_complexity")?,
get("max_cognitive_complexity")?,
))
}