agentic_codebase/temporal/
stability.rs1use std::path::Path;
7
8use serde::{Deserialize, Serialize};
9
10use super::history::ChangeHistory;
11
12#[derive(Debug, Clone)]
14pub struct StabilityOptions {
15 pub change_frequency_weight: f32,
17 pub bugfix_ratio_weight: f32,
19 pub recent_activity_weight: f32,
21 pub author_concentration_weight: f32,
23 pub churn_weight: f32,
25 pub now_timestamp: u64,
27 pub recent_window_secs: u64,
29}
30
31impl Default for StabilityOptions {
32 fn default() -> Self {
33 Self {
34 change_frequency_weight: 0.25,
35 bugfix_ratio_weight: 0.25,
36 recent_activity_weight: 0.20,
37 author_concentration_weight: 0.15,
38 churn_weight: 0.15,
39 now_timestamp: 0,
40 recent_window_secs: 30 * 24 * 3600,
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct StabilityFactor {
48 pub name: String,
50 pub value: f32,
52 pub weight: f32,
54 pub description: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct StabilityRecommendation {
61 pub priority: u32,
63 pub summary: String,
65 pub detail: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct StabilityResult {
72 pub path: String,
74 pub overall_score: f32,
76 pub factors: Vec<StabilityFactor>,
78 pub recommendations: Vec<StabilityRecommendation>,
80}
81
82#[derive(Debug, Clone)]
84pub struct StabilityAnalyzer {
85 options: StabilityOptions,
87}
88
89impl StabilityAnalyzer {
90 pub fn new() -> Self {
92 Self {
93 options: StabilityOptions::default(),
94 }
95 }
96
97 pub fn with_options(options: StabilityOptions) -> Self {
99 Self { options }
100 }
101
102 pub fn calculate_stability(&self, path: &Path, history: &ChangeHistory) -> StabilityResult {
107 let change_count = history.change_count(path);
108
109 if change_count == 0 {
111 return StabilityResult {
112 path: path.display().to_string(),
113 overall_score: 1.0,
114 factors: vec![StabilityFactor {
115 name: "no_history".to_string(),
116 value: 1.0,
117 weight: 1.0,
118 description: "No change history recorded; assumed stable.".to_string(),
119 }],
120 recommendations: Vec::new(),
121 };
122 }
123
124 let mut factors = Vec::new();
125
126 let freq_score = 1.0 / (1.0 + (change_count as f32).log2());
129 factors.push(StabilityFactor {
130 name: "change_frequency".to_string(),
131 value: freq_score,
132 weight: self.options.change_frequency_weight,
133 description: format!(
134 "{} total changes recorded; frequency score {:.2}.",
135 change_count, freq_score
136 ),
137 });
138
139 let bugfix_count = history.bugfix_count(path);
141 let bugfix_ratio = if change_count > 0 {
142 bugfix_count as f32 / change_count as f32
143 } else {
144 0.0
145 };
146 let bugfix_score = 1.0 - bugfix_ratio;
147 factors.push(StabilityFactor {
148 name: "bugfix_ratio".to_string(),
149 value: bugfix_score,
150 weight: self.options.bugfix_ratio_weight,
151 description: format!(
152 "{} of {} changes were bugfixes ({:.0}%); bugfix score {:.2}.",
153 bugfix_count,
154 change_count,
155 bugfix_ratio * 100.0,
156 bugfix_score
157 ),
158 });
159
160 let now = if self.options.now_timestamp > 0 {
162 self.options.now_timestamp
163 } else {
164 crate::types::now_micros() / 1_000_000
165 };
166 let cutoff = now.saturating_sub(self.options.recent_window_secs);
167 let changes = history.changes_for_path(path);
168 let recent_count = changes.iter().filter(|c| c.timestamp >= cutoff).count();
169 let recent_ratio = if change_count > 0 {
170 recent_count as f32 / change_count as f32
171 } else {
172 0.0
173 };
174 let recent_score = 1.0 - recent_ratio.min(1.0);
175 factors.push(StabilityFactor {
176 name: "recent_activity".to_string(),
177 value: recent_score,
178 weight: self.options.recent_activity_weight,
179 description: format!(
180 "{} of {} changes were recent (within window); recency score {:.2}.",
181 recent_count, change_count, recent_score
182 ),
183 });
184
185 let authors = history.authors_for_path(path);
187 let author_count = authors.len().max(1);
188 let author_score = 1.0 / (author_count as f32);
189 factors.push(StabilityFactor {
190 name: "author_concentration".to_string(),
191 value: author_score,
192 weight: self.options.author_concentration_weight,
193 description: format!(
194 "{} unique authors; concentration score {:.2}.",
195 author_count, author_score
196 ),
197 });
198
199 let churn = history.total_churn(path);
201 let churn_score = 1.0 / (1.0 + (churn as f32).log2().max(0.0));
202 factors.push(StabilityFactor {
203 name: "churn".to_string(),
204 value: churn_score,
205 weight: self.options.churn_weight,
206 description: format!(
207 "{} total lines churned; churn score {:.2}.",
208 churn, churn_score
209 ),
210 });
211
212 let overall_score: f32 = factors.iter().map(|f| f.value * f.weight).sum::<f32>()
214 / factors.iter().map(|f| f.weight).sum::<f32>().max(0.001);
215 let overall_score = overall_score.clamp(0.0, 1.0);
216
217 let mut recommendations = Vec::new();
219 if bugfix_ratio > 0.5 {
220 recommendations.push(StabilityRecommendation {
221 priority: 1,
222 summary: "High bugfix ratio".to_string(),
223 detail: format!(
224 "Over {:.0}% of changes are bugfixes. Consider refactoring for reliability.",
225 bugfix_ratio * 100.0
226 ),
227 });
228 }
229 if recent_count > 5 {
230 recommendations.push(StabilityRecommendation {
231 priority: 2,
232 summary: "High recent activity".to_string(),
233 detail: format!(
234 "{} changes in the recent window. This file may be in active flux.",
235 recent_count
236 ),
237 });
238 }
239 if author_count > 5 {
240 recommendations.push(StabilityRecommendation {
241 priority: 3,
242 summary: "Many authors".to_string(),
243 detail: format!(
244 "{} authors have modified this file. Consider assigning ownership.",
245 author_count
246 ),
247 });
248 }
249
250 StabilityResult {
251 path: path.display().to_string(),
252 overall_score,
253 factors,
254 recommendations,
255 }
256 }
257}
258
259impl Default for StabilityAnalyzer {
260 fn default() -> Self {
261 Self::new()
262 }
263}