1use serde::{Deserialize, Serialize};
7
8use crate::graph::CodeGraph;
9use crate::types::CodeUnitType;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CodeProphecy {
16 pub subject: ProphecySubject,
18 pub horizon: ProphecyHorizon,
20 pub predictions: Vec<EnhancedPrediction>,
22 pub confidence: f64,
24 pub evidence: Vec<ProphecyEvidence>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub enum ProphecySubject {
31 Node(u64),
33 Module(String),
35 Pattern(String),
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub enum ProphecyHorizon {
42 Immediate,
44 ShortTerm,
46 MediumTerm,
48 LongTerm,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct EnhancedPrediction {
55 pub outcome: String,
57 pub probability: f64,
59 pub sentiment: Sentiment,
61 pub trigger: String,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67pub enum Sentiment {
68 Positive,
69 Neutral,
70 Negative,
71 Critical,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ProphecyEvidence {
77 pub evidence_type: EvidenceType,
79 pub description: String,
81 pub weight: f64,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87pub enum EvidenceType {
88 Historical,
90 Structural,
92 Complexity,
94 Dependency,
96 IndustryPattern,
98}
99
100pub struct EnhancedProphecyEngine<'g> {
104 graph: &'g CodeGraph,
105}
106
107impl<'g> EnhancedProphecyEngine<'g> {
108 pub fn new(graph: &'g CodeGraph) -> Self {
109 Self { graph }
110 }
111
112 pub fn prophecy(&self, subject: ProphecySubject, horizon: ProphecyHorizon) -> CodeProphecy {
114 let (predictions, evidence) = match &subject {
115 ProphecySubject::Node(id) => self.prophesy_node(*id, horizon),
116 ProphecySubject::Module(name) => self.prophesy_module(name, horizon),
117 ProphecySubject::Pattern(name) => self.prophesy_pattern(name, horizon),
118 };
119
120 let confidence = if evidence.is_empty() {
121 0.3
122 } else {
123 let avg_weight: f64 =
124 evidence.iter().map(|e| e.weight).sum::<f64>() / evidence.len() as f64;
125 avg_weight.min(1.0)
126 };
127
128 CodeProphecy {
129 subject,
130 horizon,
131 predictions,
132 confidence,
133 evidence,
134 }
135 }
136
137 pub fn prophecy_if(
139 &self,
140 subject: ProphecySubject,
141 scenario: &str,
142 horizon: ProphecyHorizon,
143 ) -> CodeProphecy {
144 let mut prophecy = self.prophecy(subject, horizon);
145
146 prophecy.predictions.push(EnhancedPrediction {
148 outcome: format!("If {}: additional changes likely needed", scenario),
149 probability: 0.6,
150 sentiment: Sentiment::Neutral,
151 trigger: scenario.to_string(),
152 });
153
154 prophecy
155 }
156
157 pub fn prophecy_compare(
159 &self,
160 subject_a: ProphecySubject,
161 subject_b: ProphecySubject,
162 horizon: ProphecyHorizon,
163 ) -> (CodeProphecy, CodeProphecy) {
164 let a = self.prophecy(subject_a, horizon);
165 let b = self.prophecy(subject_b, horizon);
166 (a, b)
167 }
168
169 fn prophesy_node(
172 &self,
173 id: u64,
174 _horizon: ProphecyHorizon,
175 ) -> (Vec<EnhancedPrediction>, Vec<ProphecyEvidence>) {
176 let mut predictions = Vec::new();
177 let mut evidence = Vec::new();
178
179 if let Some(unit) = self.graph.get_unit(id) {
180 if unit.complexity > 15 {
182 predictions.push(EnhancedPrediction {
183 outcome: "High risk of bugs due to complexity".to_string(),
184 probability: 0.7,
185 sentiment: Sentiment::Negative,
186 trigger: format!("Cyclomatic complexity: {}", unit.complexity),
187 });
188 evidence.push(ProphecyEvidence {
189 evidence_type: EvidenceType::Complexity,
190 description: format!("Complexity score: {} (threshold: 15)", unit.complexity),
191 weight: 0.8,
192 });
193 }
194
195 if unit.change_count > 10 {
197 predictions.push(EnhancedPrediction {
198 outcome: "Frequently modified — likely needs refactoring".to_string(),
199 probability: 0.6,
200 sentiment: Sentiment::Negative,
201 trigger: format!("{} changes recorded", unit.change_count),
202 });
203 evidence.push(ProphecyEvidence {
204 evidence_type: EvidenceType::Historical,
205 description: format!("Changed {} times", unit.change_count),
206 weight: 0.7,
207 });
208 }
209
210 if unit.stability_score < 0.3 {
212 predictions.push(EnhancedPrediction {
213 outcome: "Unstable code — expect more changes".to_string(),
214 probability: 0.8,
215 sentiment: Sentiment::Negative,
216 trigger: format!("Stability score: {:.2}", unit.stability_score),
217 });
218 evidence.push(ProphecyEvidence {
219 evidence_type: EvidenceType::Structural,
220 description: format!("Stability score: {:.2}", unit.stability_score),
221 weight: 0.8,
222 });
223 }
224
225 let incoming = self.graph.edges_to(id).len();
227 let outgoing = self.graph.edges_from(id).len();
228 if incoming > 10 {
229 predictions.push(EnhancedPrediction {
230 outcome: "High coupling — changes here affect many dependents".to_string(),
231 probability: 0.75,
232 sentiment: Sentiment::Critical,
233 trigger: format!("{} incoming dependencies", incoming),
234 });
235 evidence.push(ProphecyEvidence {
236 evidence_type: EvidenceType::Dependency,
237 description: format!("{} dependents, {} dependencies", incoming, outgoing),
238 weight: 0.9,
239 });
240 }
241
242 if predictions.is_empty() {
244 predictions.push(EnhancedPrediction {
245 outcome: "Code appears stable with manageable complexity".to_string(),
246 probability: 0.7,
247 sentiment: Sentiment::Positive,
248 trigger: "No risk factors detected".to_string(),
249 });
250 }
251 }
252
253 (predictions, evidence)
254 }
255
256 fn prophesy_module(
257 &self,
258 module_name: &str,
259 _horizon: ProphecyHorizon,
260 ) -> (Vec<EnhancedPrediction>, Vec<ProphecyEvidence>) {
261 let mut predictions = Vec::new();
262 let mut evidence = Vec::new();
263
264 let module_units: Vec<_> = self
266 .graph
267 .units()
268 .iter()
269 .filter(|u| u.qualified_name.starts_with(module_name))
270 .collect();
271
272 if module_units.is_empty() {
273 predictions.push(EnhancedPrediction {
274 outcome: format!("Module '{}' not found in codebase", module_name),
275 probability: 1.0,
276 sentiment: Sentiment::Neutral,
277 trigger: "Module not indexed".to_string(),
278 });
279 return (predictions, evidence);
280 }
281
282 let avg_complexity: f64 = module_units
283 .iter()
284 .map(|u| u.complexity as f64)
285 .sum::<f64>()
286 / module_units.len() as f64;
287 let total_changes: u32 = module_units.iter().map(|u| u.change_count).sum();
288 let function_count = module_units
289 .iter()
290 .filter(|u| u.unit_type == CodeUnitType::Function)
291 .count();
292
293 evidence.push(ProphecyEvidence {
294 evidence_type: EvidenceType::Structural,
295 description: format!(
296 "{} units, {} functions, avg complexity: {:.1}",
297 module_units.len(),
298 function_count,
299 avg_complexity
300 ),
301 weight: 0.7,
302 });
303
304 if avg_complexity > 10.0 {
305 predictions.push(EnhancedPrediction {
306 outcome: "Module complexity is growing — consider refactoring".to_string(),
307 probability: 0.65,
308 sentiment: Sentiment::Negative,
309 trigger: format!("Average complexity: {:.1}", avg_complexity),
310 });
311 }
312
313 if total_changes > 50 {
314 predictions.push(EnhancedPrediction {
315 outcome: "Hotspot module — high change velocity".to_string(),
316 probability: 0.7,
317 sentiment: Sentiment::Negative,
318 trigger: format!("{} total changes across module", total_changes),
319 });
320 }
321
322 if predictions.is_empty() {
323 predictions.push(EnhancedPrediction {
324 outcome: "Module appears healthy".to_string(),
325 probability: 0.7,
326 sentiment: Sentiment::Positive,
327 trigger: "No risk factors detected".to_string(),
328 });
329 }
330
331 (predictions, evidence)
332 }
333
334 fn prophesy_pattern(
335 &self,
336 _pattern_name: &str,
337 _horizon: ProphecyHorizon,
338 ) -> (Vec<EnhancedPrediction>, Vec<ProphecyEvidence>) {
339 let predictions = vec![EnhancedPrediction {
341 outcome: "Pattern analysis requires more data points".to_string(),
342 probability: 0.5,
343 sentiment: Sentiment::Neutral,
344 trigger: "Insufficient pattern data".to_string(),
345 }];
346 let evidence = vec![ProphecyEvidence {
347 evidence_type: EvidenceType::IndustryPattern,
348 description: "Pattern-level predictions require historical commit data".to_string(),
349 weight: 0.3,
350 }];
351 (predictions, evidence)
352 }
353}
354
355#[cfg(test)]
358mod tests {
359 use super::*;
360 use crate::types::{CodeUnit, CodeUnitType, Language, Span};
361 use std::path::PathBuf;
362
363 fn test_graph() -> CodeGraph {
364 let mut graph = CodeGraph::with_default_dimension();
365 let mut unit = CodeUnit::new(
366 CodeUnitType::Function,
367 Language::Rust,
368 "complex_func".to_string(),
369 "mod::complex_func".to_string(),
370 PathBuf::from("src/complex.rs"),
371 Span::new(1, 0, 100, 0),
372 );
373 unit.complexity = 25;
374 unit.change_count = 15;
375 unit.stability_score = 0.2;
376 graph.add_unit(unit);
377 graph
378 }
379
380 #[test]
381 fn prophecy_detects_complexity() {
382 let graph = test_graph();
383 let engine = EnhancedProphecyEngine::new(&graph);
384 let prophecy = engine.prophecy(ProphecySubject::Node(0), ProphecyHorizon::ShortTerm);
385 assert!(!prophecy.predictions.is_empty());
386 assert!(prophecy
387 .predictions
388 .iter()
389 .any(|p| p.sentiment == Sentiment::Negative));
390 }
391
392 #[test]
393 fn prophecy_has_evidence() {
394 let graph = test_graph();
395 let engine = EnhancedProphecyEngine::new(&graph);
396 let prophecy = engine.prophecy(ProphecySubject::Node(0), ProphecyHorizon::MediumTerm);
397 assert!(!prophecy.evidence.is_empty());
398 }
399
400 #[test]
401 fn prophecy_compare_returns_pair() {
402 let graph = test_graph();
403 let engine = EnhancedProphecyEngine::new(&graph);
404 let (a, b) = engine.prophecy_compare(
405 ProphecySubject::Node(0),
406 ProphecySubject::Module("mod".to_string()),
407 ProphecyHorizon::LongTerm,
408 );
409 assert!(!a.predictions.is_empty());
410 assert!(!b.predictions.is_empty());
411 }
412}