Skip to main content

agentic_codebase/temporal/
prophecy_v2.rs

1//! Enhanced Code Prophecy — Invention 2.
2//!
3//! Simulate the future state of the codebase based on current trajectory
4//! and proposed changes. "Should I refactor this?" answered with data.
5
6use serde::{Deserialize, Serialize};
7
8use crate::graph::CodeGraph;
9use crate::types::CodeUnitType;
10
11// ── Types ────────────────────────────────────────────────────────────────────
12
13/// A prophecy about code evolution.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct CodeProphecy {
16    /// What we're prophesying about.
17    pub subject: ProphecySubject,
18    /// Time horizon.
19    pub horizon: ProphecyHorizon,
20    /// Predicted outcomes.
21    pub predictions: Vec<EnhancedPrediction>,
22    /// Confidence in prophecy.
23    pub confidence: f64,
24    /// Evidence supporting prophecy.
25    pub evidence: Vec<ProphecyEvidence>,
26}
27
28/// What the prophecy is about.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub enum ProphecySubject {
31    /// Specific function/class.
32    Node(u64),
33    /// Entire module.
34    Module(String),
35    /// Architectural pattern.
36    Pattern(String),
37}
38
39/// Time horizon for predictions.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub enum ProphecyHorizon {
42    /// Next few changes.
43    Immediate,
44    /// Next sprint/week.
45    ShortTerm,
46    /// Next month.
47    MediumTerm,
48    /// Next quarter.
49    LongTerm,
50}
51
52/// A single prediction.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct EnhancedPrediction {
55    /// What will happen.
56    pub outcome: String,
57    /// Probability (0.0 - 1.0).
58    pub probability: f64,
59    /// Is this good or bad?
60    pub sentiment: Sentiment,
61    /// What triggers this outcome.
62    pub trigger: String,
63}
64
65/// Sentiment of a prediction.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67pub enum Sentiment {
68    Positive,
69    Neutral,
70    Negative,
71    Critical,
72}
73
74/// Evidence supporting a prophecy.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ProphecyEvidence {
77    /// Type of evidence.
78    pub evidence_type: EvidenceType,
79    /// The evidence.
80    pub description: String,
81    /// Weight in prediction.
82    pub weight: f64,
83}
84
85/// Type of evidence.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87pub enum EvidenceType {
88    /// Historical pattern in this codebase.
89    Historical,
90    /// Structural analysis.
91    Structural,
92    /// Complexity metrics.
93    Complexity,
94    /// Dependency analysis.
95    Dependency,
96    /// Industry pattern.
97    IndustryPattern,
98}
99
100// ── EnhancedProphecyEngine ───────────────────────────────────────────────────
101
102/// Enhanced prophecy engine with evidence-backed predictions.
103pub 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    /// Generate a prophecy for a given subject and horizon.
113    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    /// "What if" scenario analysis.
138    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        // Add scenario-specific predictions
147        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    /// Compare prophecies of different approaches.
158    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    // ── Internal ─────────────────────────────────────────────────────────
170
171    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            // Complexity analysis
181            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            // Change frequency analysis
196            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            // Stability analysis
211            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            // Dependency analysis
226            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            // Default positive prediction if nothing concerning
243            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        // Find units in this module
265        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        // Pattern-level prophecy is heuristic-based
340        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// ── Tests ────────────────────────────────────────────────────────────────────
356
357#[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}