use std::collections::BTreeMap;
use std::path::Path;
use crate::core::calibration::{
Calibration, CalibrationMeta, HotspotCalibration, MetricCalibration, MetricCalibrations,
MetricFloors, FLOOR_CCN, FLOOR_COGNITIVE, FLOOR_DUPLICATION_PCT, FLOOR_OK_CCN,
FLOOR_OK_COGNITIVE, FLOOR_OK_DOC_HOTSPOT, FLOOR_OK_TEST_HOTSPOT, STRATEGY_PERCENTILE,
};
use crate::core::config::Config;
use crate::core::doc_pairs::DocPairsFile;
use crate::core::finding::Finding;
use crate::observer::code::change_coupling::{ChangeCouplingObserver, ChangeCouplingReport};
use crate::observer::code::churn::{ChurnObserver, ChurnReport};
use crate::observer::code::complexity::{ComplexityObserver, ComplexityReport};
use crate::observer::code::duplication::{
DocsDuplicationInputs, DuplicationObserver, DuplicationReport,
};
use crate::observer::code::hotspot::{compose as compose_hotspot, HotspotReport, HotspotWeights};
use crate::observer::code::lcom::{LcomObserver, LcomReport};
use crate::observer::code::loc::{LocObserver, LocReport};
use crate::observer::docs::coverage::{DocCoverageObserver, DocCoverageReport};
use crate::observer::docs::drift::{DocDriftObserver, DocDriftReport};
use crate::observer::docs::freshness::{DocFreshnessObserver, DocFreshnessReport};
use crate::observer::docs::hotspot::{compose as compose_doc_hotspot, DocHotspotReport};
use crate::observer::docs::link_health::{
paired_doc_paths, DocLinkHealthObserver, DocLinkHealthReport,
};
use crate::observer::docs::orphan_pages::{OrphanPagesObserver, OrphanPagesReport};
use crate::observer::docs::todo_density::{TodoDensityObserver, TodoDensityReport};
use crate::observer::docs::walk::walk_standalone_docs;
use crate::observer::test::coverage::{CoverageObserver, CoverageReport};
use crate::observer::test::hotspot::{compose as compose_test_hotspot, TestHotspotReport};
use crate::observer::test::skip_ratio::{SkipRatioObserver, SkipRatioReport};
use crate::cli::MetricKind;
#[derive(Default)]
pub struct ObserverReports {
pub loc: LocReport,
pub complexity: ComplexityReport,
pub complexity_observer: ComplexityObserver,
pub churn: Option<ChurnReport>,
pub change_coupling: Option<ChangeCouplingReport>,
pub duplication: Option<DuplicationReport>,
pub hotspot: Option<HotspotReport>,
pub lcom: Option<LcomReport>,
pub doc_pairs: Option<DocPairsFile>,
pub doc_freshness: Option<DocFreshnessReport>,
pub doc_drift: Option<DocDriftReport>,
pub doc_coverage: Option<DocCoverageReport>,
pub doc_link_health: Option<DocLinkHealthReport>,
pub orphan_pages: Option<OrphanPagesReport>,
pub todo_density: Option<TodoDensityReport>,
pub coverage: Option<CoverageReport>,
pub skip_ratio: Option<SkipRatioReport>,
pub test_hotspot: Option<TestHotspotReport>,
pub doc_hotspot: Option<DocHotspotReport>,
}
#[allow(clippy::too_many_lines)] pub(crate) fn run_all(
project: &Path,
cfg: &Config,
only: Option<MetricKind>,
workspace: Option<&Path>,
) -> ObserverReports {
let want = |m: MetricKind| match only {
None => true,
Some(o) if o == m => true,
Some(MetricKind::Hotspot) if matches!(m, MetricKind::Churn | MetricKind::Complexity) => {
true
}
Some(MetricKind::TestHotspot)
if matches!(m, MetricKind::Churn | MetricKind::CoveragePct) =>
{
true
}
Some(MetricKind::DocHotspot)
if matches!(
m,
MetricKind::Churn | MetricKind::DocFreshness | MetricKind::DocDrift
) =>
{
true
}
_ => false,
};
let ws_buf = workspace.map(Path::to_path_buf);
let loc_root = workspace.unwrap_or(project);
let loc = if want(MetricKind::Loc) {
LocObserver::from_config(cfg).scan(loc_root)
} else {
LocReport::default()
};
let complexity_observer = ComplexityObserver::from_config(cfg).with_workspace(ws_buf.clone());
let complexity = if want(MetricKind::Complexity) {
complexity_observer.scan(project)
} else {
ComplexityReport::default()
};
let churn = (want(MetricKind::Churn) && cfg.metrics.is_enabled("churn")).then(|| {
ChurnObserver::from_config(cfg)
.with_workspace(ws_buf.clone())
.scan(project)
});
let change_coupling = (want(MetricKind::ChangeCoupling)
&& cfg.metrics.is_enabled("change_coupling"))
.then(|| {
ChangeCouplingObserver::from_config(cfg)
.with_workspace(ws_buf.clone())
.scan(project)
})
.map(|mut report| {
crate::observer::code::change_coupling::classify_and_filter(
&mut report,
loc.primary.as_deref(),
);
report
});
let want_doc_pairs_consumer = cfg.features.docs.enabled
&& (want(MetricKind::DocFreshness)
|| want(MetricKind::DocDrift)
|| want(MetricKind::DocCoverage)
|| want(MetricKind::OrphanPages)
|| want(MetricKind::DocHotspot));
let want_doc_corpus_consumer = cfg.features.docs.enabled
&& (want(MetricKind::Duplication)
|| want(MetricKind::DocLinkHealth)
|| want(MetricKind::OrphanPages)
|| want(MetricKind::TodoDensity));
let want_docs_prep = want_doc_pairs_consumer || want_doc_corpus_consumer;
let doc_pairs = if want_docs_prep {
load_doc_pairs(project, cfg)
} else {
None
};
let standalone_docs: Vec<std::path::PathBuf> = if want_doc_corpus_consumer {
walk_standalone_docs(project, cfg)
} else {
Vec::new()
};
let paired_doc_paths_owned = paired_doc_paths(doc_pairs.as_ref());
let mut all_doc_paths: Vec<std::path::PathBuf> = standalone_docs.clone();
for p in &paired_doc_paths_owned {
if !all_doc_paths.contains(p) {
all_doc_paths.push(p.clone());
}
}
let doc_corpus: Vec<crate::observer::docs::corpus::DocBody> = if want_doc_corpus_consumer {
crate::observer::docs::corpus::read_doc_bodies(project, &all_doc_paths)
} else {
Vec::new()
};
let duplication = (want(MetricKind::Duplication) && cfg.metrics.is_enabled("duplication"))
.then(|| {
let docs_inputs = if cfg.features.docs.enabled && !standalone_docs.is_empty() {
Some(DocsDuplicationInputs {
min_tokens: cfg.metrics.duplication.docs_min_tokens,
docs: crate::observer::docs::corpus::select(&doc_corpus, &standalone_docs),
})
} else {
None
};
DuplicationObserver::from_config(cfg)
.with_workspace(ws_buf.clone())
.with_docs(docs_inputs)
.scan(project)
});
let live_pairs: Vec<_> = if want_doc_pairs_consumer {
crate::observer::docs::freshness::live_pairs(doc_pairs.as_ref(), project)
} else {
Vec::new()
};
let coverage = (want(MetricKind::CoveragePct)
&& cfg.features.test.enabled
&& cfg.features.test.coverage.enabled)
.then(|| CoverageObserver::from_config(cfg).scan(project));
let skip_ratio = (want(MetricKind::SkipRatio) && cfg.features.test.enabled)
.then(|| SkipRatioObserver::from_config(cfg).scan(project));
let hotspot = match (
want(MetricKind::Hotspot) && cfg.metrics.is_enabled("hotspot"),
churn.as_ref(),
) {
(true, Some(ch)) => Some(compose_hotspot(
ch,
&complexity,
HotspotWeights {
churn: cfg.metrics.hotspot.weight_churn,
complexity: cfg.metrics.hotspot.weight_complexity,
},
)),
_ => None,
};
let lcom = (want(MetricKind::Lcom) && cfg.metrics.is_enabled("lcom")).then(|| {
LcomObserver::from_config(cfg)
.with_workspace(ws_buf)
.scan(project)
});
let doc_freshness = (want(MetricKind::DocFreshness) && doc_pairs.is_some()).then(|| {
DocFreshnessObserver::from_config_and_pairs(cfg, live_pairs.clone()).scan(project)
});
let doc_drift = (want(MetricKind::DocDrift) && doc_pairs.is_some())
.then(|| DocDriftObserver::from_config_and_pairs(cfg, live_pairs.clone()).scan(project));
let doc_coverage = (want(MetricKind::DocCoverage) && doc_pairs.is_some()).then(|| {
let pairs = doc_pairs
.as_ref()
.map(|f| f.pairs.clone())
.unwrap_or_default();
DocCoverageObserver::from_config_and_pairs(cfg, pairs).scan(project)
});
let doc_link_health = (want(MetricKind::DocLinkHealth) && cfg.features.docs.enabled)
.then(|| DocLinkHealthObserver::from_inputs(cfg, doc_corpus.clone()).scan(project));
let orphan_pages = (want(MetricKind::OrphanPages) && cfg.features.docs.enabled).then(|| {
let standalone = crate::observer::docs::corpus::select(&doc_corpus, &standalone_docs);
OrphanPagesObserver::from_inputs(cfg, standalone, paired_doc_paths_owned.clone()).scan()
});
let todo_density = (want(MetricKind::TodoDensity) && cfg.features.docs.enabled).then(|| {
TodoDensityObserver::from_inputs(cfg, doc_corpus.clone()).scan()
});
let test_hotspot = (want(MetricKind::TestHotspot)
&& cfg.features.test.enabled
&& cfg.features.test.coverage.enabled)
.then(|| {
let ch = churn.as_ref();
let cov = coverage.as_ref();
ch.map(|ch| compose_test_hotspot(ch, cov))
})
.flatten();
let doc_hotspot =
(want(MetricKind::DocHotspot) && cfg.features.docs.enabled && doc_pairs.is_some())
.then(|| {
let ch = churn.as_ref()?;
let fr = doc_freshness.as_ref()?;
let dr = doc_drift.as_ref();
Some(compose_doc_hotspot(
ch,
fr,
dr,
cfg.features.docs.hotspot.weight_drift,
))
})
.flatten();
ObserverReports {
loc,
complexity,
complexity_observer,
churn,
change_coupling,
duplication,
hotspot,
lcom,
doc_pairs,
doc_freshness,
doc_drift,
doc_coverage,
doc_link_health,
orphan_pages,
todo_density,
coverage,
skip_ratio,
test_hotspot,
doc_hotspot,
}
}
fn load_doc_pairs(project: &Path, cfg: &Config) -> Option<DocPairsFile> {
let pairs_path = &cfg.features.docs.pairs_path;
match DocPairsFile::read(project, pairs_path) {
Ok(Some(file)) => {
for warning in file.integrity_check(project) {
eprintln!(
"warn: {pairs_path}: pair[{}] references missing path {}",
warning.pair_index,
warning.missing_path.display(),
);
}
Some(file)
}
Ok(None) => {
eprintln!(
"warn: {pairs_path} not found — run `claude /heal-doc-pair-setup` to generate it"
);
None
}
Err(err) => {
eprintln!("warn: {pairs_path}: {err}");
None
}
}
}
pub(crate) fn build_calibration(
project: &Path,
reports: &ObserverReports,
config: &Config,
) -> Calibration {
let workspaces = &config.project.workspaces;
let global_filter = |file: &Path| -> bool {
workspaces.is_empty() || crate::core::config::assign_workspace(file, workspaces).is_none()
};
let global_metrics = build_metric_calibrations(reports, config, &global_filter);
let mut workspace_metrics: BTreeMap<String, MetricCalibrations> = BTreeMap::new();
for ws in workspaces {
let ws_path = ws.path.trim_end_matches('/').to_string();
let in_workspace = |file: &Path| -> bool {
crate::core::config::assign_workspace(file, workspaces).is_some_and(|w| w == ws_path)
};
let table = build_metric_calibrations(reports, config, &in_workspace);
if has_any_table(&table) {
workspace_metrics.insert(ws_path, table);
}
}
let codebase_files = u32::try_from(
reports
.complexity
.totals
.files
.max(reports.loc.total_files()),
)
.unwrap_or(u32::MAX);
Calibration {
meta: CalibrationMeta {
created_at: chrono::Utc::now(),
codebase_files,
strategy: STRATEGY_PERCENTILE.to_owned(),
calibrated_at_sha: crate::observer::shared::git::head_sha(project),
},
calibration: global_metrics,
workspaces: workspace_metrics,
}
.with_overrides(config)
}
#[allow(clippy::too_many_lines)] fn build_metric_calibrations(
reports: &ObserverReports,
config: &Config,
file_filter: &dyn Fn(&Path) -> bool,
) -> MetricCalibrations {
let ccn_floors = MetricFloors {
critical: Some(FLOOR_CCN),
ok: Some(FLOOR_OK_CCN),
};
let cognitive_floors = MetricFloors {
critical: Some(FLOOR_COGNITIVE),
ok: Some(FLOOR_OK_COGNITIVE),
};
let duplication_floors = MetricFloors {
critical: Some(FLOOR_DUPLICATION_PCT),
ok: None,
};
let ccn = if config.metrics.is_enabled("ccn") {
let values: Vec<f64> = reports
.complexity
.files
.iter()
.filter(|f| file_filter(&f.path))
.flat_map(|f| f.functions.iter().map(|fun| f64::from(fun.ccn)))
.collect();
non_empty(&values).then(|| MetricCalibration::from_distribution(&values, ccn_floors))
} else {
None
};
let cognitive = if config.metrics.is_enabled("cognitive") {
let values: Vec<f64> = reports
.complexity
.files
.iter()
.filter(|f| file_filter(&f.path))
.flat_map(|f| f.functions.iter().map(|fun| f64::from(fun.cognitive)))
.collect();
non_empty(&values).then(|| MetricCalibration::from_distribution(&values, cognitive_floors))
} else {
None
};
let duplication = reports.duplication.as_ref().and_then(|d| {
let values: Vec<f64> = d
.files
.iter()
.filter(|f| file_filter(&f.path))
.map(|f| f.duplicate_pct)
.collect();
non_empty(&values)
.then(|| MetricCalibration::from_distribution(&values, duplication_floors))
});
let change_coupling = reports.change_coupling.as_ref().and_then(|c| {
let values: Vec<f64> = c
.pairs
.iter()
.filter(|p| file_filter(&p.a) && file_filter(&p.b))
.map(|p| f64::from(p.count))
.collect();
non_empty(&values)
.then(|| MetricCalibration::from_distribution(&values, MetricFloors::default()))
});
let hotspot = reports.hotspot.as_ref().and_then(|h| {
let scores: Vec<f64> = h
.entries
.iter()
.filter(|e| file_filter(&e.path))
.map(|e| e.score)
.collect();
non_empty(&scores).then(|| HotspotCalibration::from_distribution(&scores))
});
let lcom = reports.lcom.as_ref().and_then(|l| {
let values: Vec<f64> = l
.classes
.iter()
.filter(|c| file_filter(&c.file))
.map(|c| f64::from(c.cluster_count))
.collect();
non_empty(&values)
.then(|| MetricCalibration::from_distribution(&values, MetricFloors::default()))
});
let coverage_pct_floors = MetricFloors {
critical: Some(95.0),
ok: Some(25.0),
};
let coverage_pct = reports.coverage.as_ref().and_then(|c| {
let values: Vec<f64> = c
.entries
.iter()
.filter(|e| file_filter(&e.path))
.map(|e| 100.0 - e.line_coverage_pct)
.collect();
non_empty(&values)
.then(|| MetricCalibration::from_distribution(&values, coverage_pct_floors))
});
let skip_ratio_floors = MetricFloors {
critical: Some(20.0),
ok: Some(0.5),
};
let skip_ratio = reports.skip_ratio.as_ref().and_then(|s| {
let values: Vec<f64> = s
.entries
.iter()
.filter(|e| file_filter(&e.path) && e.skipped_tests > 0)
.map(|e| e.skip_pct)
.collect();
non_empty(&values).then(|| MetricCalibration::from_distribution(&values, skip_ratio_floors))
});
let test_hotspot = reports.test_hotspot.as_ref().and_then(|h| {
let scores: Vec<f64> = h
.entries
.iter()
.filter(|e| file_filter(&e.path))
.map(|e| e.score)
.collect();
non_empty(&scores).then(|| {
HotspotCalibration::from_distribution_with_floor(&scores, Some(FLOOR_OK_TEST_HOTSPOT))
})
});
let doc_hotspot = reports.doc_hotspot.as_ref().and_then(|h| {
let scores: Vec<f64> = h
.entries
.iter()
.filter(|e| file_filter(&e.doc_path))
.map(|e| e.score)
.collect();
non_empty(&scores).then(|| {
HotspotCalibration::from_distribution_with_floor(&scores, Some(FLOOR_OK_DOC_HOTSPOT))
})
});
MetricCalibrations {
ccn,
cognitive,
duplication,
change_coupling,
hotspot,
lcom,
coverage_pct,
skip_ratio,
test_hotspot,
doc_hotspot,
}
}
fn has_any_table(m: &MetricCalibrations) -> bool {
m.ccn.is_some()
|| m.cognitive.is_some()
|| m.duplication.is_some()
|| m.change_coupling.is_some()
|| m.hotspot.is_some()
|| m.lcom.is_some()
|| m.coverage_pct.is_some()
|| m.skip_ratio.is_some()
|| m.test_hotspot.is_some()
|| m.doc_hotspot.is_some()
}
pub(crate) fn classify(reports: &ObserverReports, cal: &Calibration, cfg: &Config) -> Vec<Finding> {
let mut findings = crate::feature::FeatureRegistry::builtin().lower_all(reports, cfg, cal);
let workspaces = &cfg.project.workspaces;
if !workspaces.is_empty() {
for f in &mut findings {
f.workspace = crate::core::config::assign_workspace(&f.location.file, workspaces)
.map(str::to_owned);
}
}
findings
}
pub(crate) fn build_record(
scan_root: &Path,
paths: &crate::core::HealPaths,
cfg: &Config,
head_sha: Option<String>,
worktree_clean: bool,
) -> crate::core::findings_cache::FindingsRecord {
let calibration = Calibration::load(&paths.calibration())
.ok()
.map(|c| c.with_overrides(cfg));
let owned;
let cal_ref = if let Some(c) = calibration.as_ref() {
c
} else {
owned = Calibration::default();
&owned
};
let reports = run_all(scan_root, cfg, None, None);
let findings = classify(&reports, cal_ref, cfg);
let config_hash =
crate::core::findings_cache::config_hash_from_paths(&paths.config(), &paths.calibration());
crate::core::findings_cache::FindingsRecord::new(
head_sha,
worktree_clean,
config_hash,
findings,
)
}
fn non_empty(values: &[f64]) -> bool {
!values.is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::calibration::{CalibrationMeta, MetricCalibrations, STRATEGY_PERCENTILE};
use crate::core::finding::IntoFindings;
use crate::core::severity::Severity;
use crate::core::severity::SeverityCounts;
use crate::observer::code::change_coupling::{ChangeCouplingReport, CouplingTotals, FilePair};
use crate::observer::code::complexity::{
ComplexityReport, ComplexityTotals, FileComplexity, FunctionMetric,
};
use crate::observer::code::duplication::{
DuplicateBlock, DuplicateLocation, DuplicationReport, DuplicationTotals, FileDuplication,
};
use crate::observer::code::hotspot::{HotspotEntry, HotspotReport, HotspotTotals};
use std::collections::HashSet;
use std::path::PathBuf;
#[allow(clippy::too_many_lines)] fn fixture() -> (ObserverReports, Calibration) {
let complexity = ComplexityReport {
files: vec![FileComplexity {
path: PathBuf::from("src/hot.rs"),
language: "rust".into(),
functions: vec![
FunctionMetric {
name: "tangled".into(),
start_line: 10,
end_line: 80,
ccn: 30,
cognitive: 60,
},
FunctionMetric {
name: "tidy".into(),
start_line: 100,
end_line: 110,
ccn: 2,
cognitive: 1,
},
],
}],
totals: ComplexityTotals {
files: 1,
functions: 2,
max_ccn: 30,
max_cognitive: 60,
},
};
let duplication = DuplicationReport {
blocks: vec![DuplicateBlock {
token_count: 80,
locations: vec![
DuplicateLocation {
path: PathBuf::from("src/hot.rs"),
start_line: 200,
end_line: 220,
},
DuplicateLocation {
path: PathBuf::from("src/cold.rs"),
start_line: 5,
end_line: 25,
},
],
}],
files: vec![
FileDuplication {
path: PathBuf::from("src/hot.rs"),
total_tokens: 200,
duplicate_tokens: 80,
duplicate_pct: 40.0,
},
FileDuplication {
path: PathBuf::from("src/cold.rs"),
total_tokens: 200,
duplicate_tokens: 10,
duplicate_pct: 5.0,
},
],
totals: DuplicationTotals {
duplicate_blocks: 1,
duplicate_tokens: 90,
files_affected: 2,
},
min_tokens: 50,
};
let change_coupling = ChangeCouplingReport {
pairs: vec![FilePair {
a: PathBuf::from("src/cold.rs"),
b: PathBuf::from("src/hot.rs"),
count: 12,
direction: None,
class: None,
}],
file_sums: Vec::new(),
totals: CouplingTotals {
pairs: 1,
files: 2,
commits_considered: 50,
},
since_days: 90,
min_coupling: 3,
};
let hotspot = HotspotReport {
entries: vec![
HotspotEntry {
path: PathBuf::from("src/hot.rs"),
ccn_sum: 32,
churn_commits: 20,
score: 640.0,
},
HotspotEntry {
path: PathBuf::from("src/cold.rs"),
ccn_sum: 4,
churn_commits: 2,
score: 8.0,
},
],
totals: HotspotTotals {
files: 2,
max_score: 640.0,
},
};
let reports = ObserverReports {
complexity,
change_coupling: Some(change_coupling),
duplication: Some(duplication),
hotspot: Some(hotspot),
..ObserverReports::default()
};
let cal = Calibration {
meta: CalibrationMeta {
created_at: chrono::Utc::now(),
codebase_files: 2,
strategy: STRATEGY_PERCENTILE.to_owned(),
calibrated_at_sha: None,
},
calibration: MetricCalibrations {
ccn: Some(MetricCalibration {
p50: 1.0,
p75: 5.0,
p90: 10.0,
p95: 20.0,
floor_critical: Some(FLOOR_CCN),
floor_ok: Some(FLOOR_OK_CCN),
}),
cognitive: Some(MetricCalibration {
p50: 1.0,
p75: 10.0,
p90: 30.0,
p95: 50.0,
floor_critical: Some(FLOOR_COGNITIVE),
floor_ok: Some(FLOOR_OK_COGNITIVE),
}),
duplication: Some(MetricCalibration {
p50: 5.0,
p75: 10.0,
p90: 20.0,
p95: 35.0,
floor_critical: Some(FLOOR_DUPLICATION_PCT),
floor_ok: None,
}),
change_coupling: Some(MetricCalibration {
p50: 1.0,
p75: 4.0,
p90: 8.0,
p95: 16.0,
floor_critical: None,
floor_ok: None,
}),
hotspot: Some(HotspotCalibration {
p50: 8.0,
p75: 20.0,
p90: 100.0,
p95: 500.0,
floor_ok: Some(crate::core::calibration::FLOOR_OK_HOTSPOT),
}),
lcom: None,
coverage_pct: None,
skip_ratio: None,
test_hotspot: None,
doc_hotspot: None,
},
workspaces: BTreeMap::new(),
};
(reports, cal)
}
#[test]
fn classify_id_set_matches_into_findings() {
let (reports, cal) = fixture();
let mut want: HashSet<String> = reports
.complexity
.into_findings()
.into_iter()
.map(|f| f.id)
.collect();
if let Some(d) = reports.duplication.as_ref() {
want.extend(d.into_findings().into_iter().map(|f| f.id));
}
if let Some(c) = reports.change_coupling.as_ref() {
want.extend(c.into_findings().into_iter().map(|f| f.id));
}
if let Some(h) = reports.hotspot.as_ref() {
want.extend(h.into_findings().into_iter().map(|f| f.id));
}
let got: HashSet<String> = classify(&reports, &cal, &Config::default())
.into_iter()
.map(|f| f.id)
.collect();
assert_eq!(
got, want,
"classify must produce the same Finding.id set as IntoFindings",
);
}
#[test]
fn classify_assigns_severity_per_metric() {
let (reports, cal) = fixture();
let findings = classify(&reports, &cal, &Config::default());
let by_metric_severity = |metric: &str, severity: Severity| {
findings
.iter()
.filter(|f| f.metric == metric && f.severity == severity)
.count()
};
assert!(by_metric_severity("ccn", Severity::Critical) >= 1);
assert!(by_metric_severity("ccn", Severity::Ok) >= 1);
assert!(by_metric_severity("cognitive", Severity::Critical) >= 1);
assert!(by_metric_severity("cognitive", Severity::Ok) >= 1);
assert!(by_metric_severity("duplication", Severity::Critical) >= 1);
assert!(by_metric_severity("change_coupling", Severity::High) >= 1);
assert!(findings
.iter()
.filter(|f| f.metric == "hotspot")
.all(|f| f.severity == Severity::Ok));
}
#[test]
fn classify_flags_hotspot_for_files_above_p90() {
let (reports, cal) = fixture();
let findings = classify(&reports, &cal, &Config::default());
let hot_path = PathBuf::from("src/hot.rs");
let cold_path = PathBuf::from("src/cold.rs");
assert!(findings
.iter()
.filter(
|f| matches!(f.metric.as_str(), "ccn" | "cognitive") && f.location.file == hot_path
)
.all(|f| f.hotspot));
assert!(findings
.iter()
.filter(|f| f.metric == "duplication")
.all(|f| f.hotspot));
let pair = findings
.iter()
.find(|f| f.metric == "change_coupling")
.expect("coupling finding present");
assert_eq!(pair.location.file, cold_path);
assert!(
pair.hotspot,
"coupling hotspot should consider partner file"
);
}
#[test]
fn classify_with_missing_calibration_falls_back_to_ok() {
let (reports, _) = fixture();
let bare = Calibration {
meta: CalibrationMeta {
created_at: chrono::Utc::now(),
codebase_files: 0,
strategy: STRATEGY_PERCENTILE.to_owned(),
calibrated_at_sha: None,
},
calibration: MetricCalibrations::default(),
workspaces: BTreeMap::new(),
};
let findings = classify(&reports, &bare, &Config::default());
assert!(
findings.iter().all(|f| f.severity == Severity::Ok),
"without calibration, every Finding must be Severity::Ok",
);
assert!(
findings.iter().all(|f| !f.hotspot),
"without hotspot calibration, the flag must stay false",
);
}
#[test]
fn severity_counts_from_findings_matches_classify_count() {
let (reports, cal) = fixture();
let findings = classify(&reports, &cal, &Config::default());
let counts = SeverityCounts::from_findings(&findings);
let total_classified = counts.critical + counts.high + counts.medium + counts.ok;
assert_eq!(
total_classified as usize,
findings.len(),
"tally must equal Finding count produced by classify",
);
}
}