use std::path::Path;
use serde::{Deserialize, Serialize};
use super::coupling::{CouplingDetector, CouplingOptions};
use super::history::ChangeHistory;
use super::stability::StabilityAnalyzer;
use crate::graph::CodeGraph;
#[derive(Debug, Clone)]
pub struct ProphecyOptions {
pub top_k: usize,
pub min_risk: f32,
pub now_timestamp: u64,
pub recent_window_secs: u64,
}
impl Default for ProphecyOptions {
fn default() -> Self {
Self {
top_k: 20,
min_risk: 0.3,
now_timestamp: 0,
recent_window_secs: 30 * 24 * 3600,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PredictionType {
BugRisk,
ChangeVelocity,
ComplexityGrowth,
CouplingRisk,
}
impl std::fmt::Display for PredictionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BugRisk => write!(f, "bug-risk"),
Self::ChangeVelocity => write!(f, "change-velocity"),
Self::ComplexityGrowth => write!(f, "complexity-growth"),
Self::CouplingRisk => write!(f, "coupling-risk"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Prediction {
pub path: String,
pub risk_score: f32,
pub prediction_type: PredictionType,
pub reason: String,
pub factors: Vec<(String, f32)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AlertType {
Hotspot,
CouplingHub,
SystemicInstability,
}
impl std::fmt::Display for AlertType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hotspot => write!(f, "hotspot"),
Self::CouplingHub => write!(f, "coupling-hub"),
Self::SystemicInstability => write!(f, "systemic-instability"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EcosystemAlert {
pub alert_type: AlertType,
pub severity: f32,
pub message: String,
pub affected_paths: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProphecyResult {
pub predictions: Vec<Prediction>,
pub alerts: Vec<EcosystemAlert>,
pub average_risk: f32,
pub files_analysed: usize,
}
#[derive(Debug, Clone)]
pub struct ProphecyEngine {
options: ProphecyOptions,
}
impl ProphecyEngine {
pub fn new() -> Self {
Self {
options: ProphecyOptions::default(),
}
}
pub fn with_options(options: ProphecyOptions) -> Self {
Self { options }
}
pub fn predict(&self, history: &ChangeHistory, graph: Option<&CodeGraph>) -> ProphecyResult {
let all_paths = history.all_paths();
let mut predictions = Vec::new();
let mut total_risk = 0.0_f32;
let stability_analyzer = StabilityAnalyzer::new();
let coupling_detector = CouplingDetector::with_options(CouplingOptions {
min_cochanges: 2,
min_strength: 0.3,
limit: 0,
});
let couplings = coupling_detector.detect_all(history, graph);
for path in &all_paths {
let stability = stability_analyzer.calculate_stability(path, history);
let velocity = self.calculate_velocity(path, history);
let bugfix_trend = self.calculate_bugfix_trend(path, history);
let complexity_growth = self.calculate_complexity_growth(path, history);
let coupling_risk = self.calculate_coupling_risk(path, &couplings);
let risk_score = (velocity * 0.30
+ bugfix_trend * 0.30
+ complexity_growth * 0.15
+ coupling_risk * 0.25)
.clamp(0.0, 1.0);
total_risk += risk_score;
let factors = vec![
("velocity".to_string(), velocity),
("bugfix_trend".to_string(), bugfix_trend),
("complexity_growth".to_string(), complexity_growth),
("coupling_risk".to_string(), coupling_risk),
];
let prediction_type = if bugfix_trend >= velocity
&& bugfix_trend >= complexity_growth
&& bugfix_trend >= coupling_risk
{
PredictionType::BugRisk
} else if coupling_risk >= velocity && coupling_risk >= complexity_growth {
PredictionType::CouplingRisk
} else if complexity_growth >= velocity {
PredictionType::ComplexityGrowth
} else {
PredictionType::ChangeVelocity
};
let reason = match &prediction_type {
PredictionType::BugRisk => format!(
"High bugfix trend ({:.2}) with stability score {:.2}.",
bugfix_trend, stability.overall_score
),
PredictionType::ChangeVelocity => format!(
"High change velocity ({:.2}); file changes frequently.",
velocity
),
PredictionType::ComplexityGrowth => format!(
"Complexity growth signal ({:.2}) from increasing churn.",
complexity_growth
),
PredictionType::CouplingRisk => format!(
"Coupling risk ({:.2}); many co-changing dependencies.",
coupling_risk
),
};
if risk_score >= self.options.min_risk {
predictions.push(Prediction {
path: path.display().to_string(),
risk_score,
prediction_type,
reason,
factors,
});
}
}
predictions.sort_by(|a, b| {
b.risk_score
.partial_cmp(&a.risk_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
if self.options.top_k > 0 {
predictions.truncate(self.options.top_k);
}
let files_analysed = all_paths.len();
let average_risk = if files_analysed > 0 {
total_risk / files_analysed as f32
} else {
0.0
};
let alerts = self.generate_alerts(history, &predictions, average_risk);
ProphecyResult {
predictions,
alerts,
average_risk,
files_analysed,
}
}
fn calculate_velocity(&self, path: &Path, history: &ChangeHistory) -> f32 {
let changes = history.changes_for_path(path);
if changes.is_empty() {
return 0.0;
}
let now = self.effective_now();
let cutoff = now.saturating_sub(self.options.recent_window_secs);
let recent_count = changes.iter().filter(|c| c.timestamp >= cutoff).count();
let total_count = changes.len();
let recent_ratio = recent_count as f32 / total_count.max(1) as f32;
let freq_factor = (recent_count as f32 / 5.0).min(1.0);
(recent_ratio * 0.5 + freq_factor * 0.5).min(1.0)
}
fn calculate_bugfix_trend(&self, path: &Path, history: &ChangeHistory) -> f32 {
let changes = history.changes_for_path(path);
if changes.is_empty() {
return 0.0;
}
let bugfix_count = changes.iter().filter(|c| c.is_bugfix).count();
let total = changes.len();
let ratio = bugfix_count as f32 / total as f32;
let now = self.effective_now();
let cutoff = now.saturating_sub(self.options.recent_window_secs);
let recent_bugfixes = changes
.iter()
.filter(|c| c.is_bugfix && c.timestamp >= cutoff)
.count();
let recent_total = changes.iter().filter(|c| c.timestamp >= cutoff).count();
let recent_ratio = if recent_total > 0 {
recent_bugfixes as f32 / recent_total as f32
} else {
0.0
};
(ratio * 0.4 + recent_ratio * 0.6).min(1.0)
}
fn calculate_complexity_growth(&self, path: &Path, history: &ChangeHistory) -> f32 {
let changes = history.changes_for_path(path);
if changes.is_empty() {
return 0.0;
}
let total_added: u64 = changes.iter().map(|c| c.lines_added as u64).sum();
let total_deleted: u64 = changes.iter().map(|c| c.lines_deleted as u64).sum();
let net_growth = if total_added > total_deleted {
(total_added - total_deleted) as f32
} else {
0.0
};
let growth_signal = net_growth / (net_growth + 100.0);
growth_signal.min(1.0)
}
fn calculate_coupling_risk(&self, path: &Path, couplings: &[super::coupling::Coupling]) -> f32 {
let path_str = path.to_path_buf();
let relevant: Vec<f32> = couplings
.iter()
.filter(|c| c.path_a == path_str || c.path_b == path_str)
.map(|c| c.strength)
.collect();
if relevant.is_empty() {
return 0.0;
}
let avg_strength: f32 = relevant.iter().sum::<f32>() / relevant.len() as f32;
let count_factor = (relevant.len() as f32).sqrt() / 3.0;
(avg_strength * 0.6 + count_factor.min(1.0) * 0.4).min(1.0)
}
fn effective_now(&self) -> u64 {
if self.options.now_timestamp > 0 {
self.options.now_timestamp
} else {
crate::types::now_micros() / 1_000_000
}
}
fn generate_alerts(
&self,
history: &ChangeHistory,
predictions: &[Prediction],
average_risk: f32,
) -> Vec<EcosystemAlert> {
let mut alerts = Vec::new();
if average_risk > 0.6 {
let affected: Vec<String> =
predictions.iter().take(5).map(|p| p.path.clone()).collect();
alerts.push(EcosystemAlert {
alert_type: AlertType::SystemicInstability,
severity: average_risk.min(1.0),
message: format!(
"Systemic instability detected: average risk {:.2} across {} files.",
average_risk,
history.all_paths().len()
),
affected_paths: affected,
});
}
for pred in predictions.iter().filter(|p| p.risk_score > 0.7) {
alerts.push(EcosystemAlert {
alert_type: AlertType::Hotspot,
severity: pred.risk_score,
message: format!(
"Hotspot detected: {} (risk {:.2}).",
pred.path, pred.risk_score
),
affected_paths: vec![pred.path.clone()],
});
}
alerts
}
}
impl Default for ProphecyEngine {
fn default() -> Self {
Self::new()
}
}