agentic_codebase/temporal/
coupling.rs1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use serde::{Deserialize, Serialize};
10
11use super::history::ChangeHistory;
12use crate::graph::CodeGraph;
13
14#[derive(Debug, Clone)]
16pub struct CouplingOptions {
17 pub min_cochanges: usize,
19 pub min_strength: f32,
21 pub limit: usize,
23}
24
25impl Default for CouplingOptions {
26 fn default() -> Self {
27 Self {
28 min_cochanges: 3,
29 min_strength: 0.5,
30 limit: 0,
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub enum CouplingType {
38 CoChange,
40 SharedBugs,
42 ExplicitEdge,
44}
45
46impl std::fmt::Display for CouplingType {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::CoChange => write!(f, "co-change"),
50 Self::SharedBugs => write!(f, "shared-bugs"),
51 Self::ExplicitEdge => write!(f, "explicit-edge"),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct Coupling {
59 pub path_a: PathBuf,
61 pub path_b: PathBuf,
63 pub cochange_count: usize,
65 pub strength: f32,
67 pub coupling_type: CouplingType,
69}
70
71#[derive(Debug, Clone)]
73pub struct CouplingDetector {
74 options: CouplingOptions,
76}
77
78impl CouplingDetector {
79 pub fn new() -> Self {
81 Self {
82 options: CouplingOptions::default(),
83 }
84 }
85
86 pub fn with_options(options: CouplingOptions) -> Self {
88 Self { options }
89 }
90
91 pub fn detect_all(&self, history: &ChangeHistory, graph: Option<&CodeGraph>) -> Vec<Coupling> {
96 let matrix = self.build_cochange_matrix(history);
97 let mut couplings = Vec::new();
98
99 for ((path_a, path_b), count) in &matrix {
100 if *count < self.options.min_cochanges {
101 continue;
102 }
103
104 let changes_a = history.change_count(path_a).max(1);
106 let changes_b = history.change_count(path_b).max(1);
107 let max_changes = changes_a.max(changes_b) as f32;
108 let strength = (*count as f32 / max_changes).min(1.0);
109
110 if strength < self.options.min_strength {
111 continue;
112 }
113
114 let coupling_type = if let Some(g) = graph {
115 if self.has_explicit_edge(g, path_a, path_b) {
116 CouplingType::ExplicitEdge
117 } else {
118 CouplingType::CoChange
119 }
120 } else {
121 CouplingType::CoChange
122 };
123
124 couplings.push(Coupling {
125 path_a: path_a.clone(),
126 path_b: path_b.clone(),
127 cochange_count: *count,
128 strength,
129 coupling_type,
130 });
131 }
132
133 couplings.sort_by(|a, b| {
135 b.strength
136 .partial_cmp(&a.strength)
137 .unwrap_or(std::cmp::Ordering::Equal)
138 });
139
140 if self.options.limit > 0 {
141 couplings.truncate(self.options.limit);
142 }
143
144 couplings
145 }
146
147 fn build_cochange_matrix(&self, history: &ChangeHistory) -> HashMap<(PathBuf, PathBuf), usize> {
152 let mut matrix: HashMap<(PathBuf, PathBuf), usize> = HashMap::new();
153
154 for commit_id in history.all_commits() {
155 let changes = history.files_in_commit(commit_id);
156 let mut paths: Vec<&Path> = changes.iter().map(|c| c.path.as_path()).collect();
157 paths.sort();
158 paths.dedup();
159
160 for i in 0..paths.len() {
162 for j in (i + 1)..paths.len() {
163 let key = (paths[i].to_path_buf(), paths[j].to_path_buf());
164 *matrix.entry(key).or_insert(0) += 1;
165 }
166 }
167 }
168
169 matrix
170 }
171
172 fn has_explicit_edge(&self, graph: &CodeGraph, path_a: &Path, path_b: &Path) -> bool {
174 let units_a = self.find_units_for_path(graph, path_a);
175 let units_b = self.find_units_for_path(graph, path_b);
176
177 for &a_id in &units_a {
178 for edge in graph.edges_from(a_id) {
179 if units_b.contains(&edge.target_id) {
180 return true;
181 }
182 }
183 }
184 false
185 }
186
187 fn find_units_for_path(&self, graph: &CodeGraph, path: &Path) -> Vec<u64> {
189 graph
190 .find_units_by_path(path)
191 .iter()
192 .map(|u| u.id)
193 .collect()
194 }
195}
196
197impl Default for CouplingDetector {
198 fn default() -> Self {
199 Self::new()
200 }
201}