use std::collections::{BTreeMap, BTreeSet};
use sdivi_snapshot::change_coupling::{ChangeCouplingResult, CoChangePair};
use crate::error::AnalysisError;
use crate::input::{ChangeCouplingConfigInput, CoChangeEventInput};
pub fn compute_change_coupling(
events: &[CoChangeEventInput],
cfg: &ChangeCouplingConfigInput,
) -> Result<ChangeCouplingResult, AnalysisError> {
if cfg.min_frequency < 0.0 || cfg.min_frequency > 1.0 {
return Err(AnalysisError::InvalidConfig {
message: format!(
"min_frequency must be in [0.0, 1.0], got {}",
cfg.min_frequency
),
});
}
let history = cfg.history_depth as usize;
let window: &[CoChangeEventInput] = if events.len() > history {
&events[events.len() - history..]
} else {
events
};
let commits_analyzed = window.len() as u32;
if commits_analyzed == 0 {
return Ok(ChangeCouplingResult {
pairs: vec![],
commits_analyzed: 0,
distinct_files_touched: 0,
});
}
let mut pair_counts: BTreeMap<(String, String), u32> = BTreeMap::new();
let mut all_files: BTreeSet<String> = BTreeSet::new();
for event in window {
for f in &event.files {
all_files.insert(f.clone());
}
let mut sorted_files = event.files.clone();
sorted_files.sort_unstable();
sorted_files.dedup();
for i in 0..sorted_files.len() {
for j in (i + 1)..sorted_files.len() {
let key = (sorted_files[i].clone(), sorted_files[j].clone());
*pair_counts.entry(key).or_insert(0) += 1;
}
}
}
let distinct_files_touched = all_files.len() as u32;
let denom = commits_analyzed as f64;
let mut pairs: Vec<CoChangePair> = pair_counts
.into_iter()
.filter_map(|((source, target), count)| {
if count < 2 {
return None;
}
let frequency = count as f64 / denom;
if frequency < cfg.min_frequency {
return None;
}
Some(CoChangePair {
source,
target,
frequency,
cochange_count: count,
})
})
.collect();
pairs.sort_unstable_by(|a, b| (&a.source, &a.target).cmp(&(&b.source, &b.target)));
Ok(ChangeCouplingResult {
pairs,
commits_analyzed,
distinct_files_touched,
})
}