mockforge_contracts/contract_drift/forecasting/
forecaster.rs1use super::pattern_analyzer::PatternAnalyzer;
7use super::statistical_model::StatisticalModel;
8use super::types::{ChangeForecast, ForecastingConfig, PatternAnalysis};
9use chrono::{DateTime, Duration, Utc};
10use mockforge_foundation::incidents_types::DriftIncident;
11
12pub struct Forecaster {
14 pattern_analyzer: PatternAnalyzer,
16 statistical_model: StatisticalModel,
18 config: ForecastingConfig,
20}
21
22impl Forecaster {
23 pub fn new(config: ForecastingConfig) -> Self {
25 let pattern_analyzer = PatternAnalyzer::new(
26 config.min_incidents_for_forecast,
27 config.pattern_confidence_threshold,
28 );
29 let statistical_model = StatisticalModel::new();
30
31 Self {
32 pattern_analyzer,
33 statistical_model,
34 config,
35 }
36 }
37
38 #[allow(clippy::too_many_arguments)]
40 pub fn generate_forecast(
41 &self,
42 incidents: &[DriftIncident],
43 _workspace_id: Option<String>,
44 service_id: Option<String>,
45 service_name: Option<String>,
46 endpoint: String,
47 method: String,
48 forecast_window_days: u32,
49 ) -> Option<ChangeForecast> {
50 if !self.config.enabled {
51 return None;
52 }
53
54 if incidents.len() < self.config.min_incidents_for_forecast {
55 return None;
56 }
57
58 let mut analyses = Vec::new();
60 let now = Utc::now();
61
62 for &window_days in &self.config.analysis_windows {
63 let window_start = now - Duration::days(window_days as i64);
64 let window_end = now;
65
66 let analysis =
67 self.pattern_analyzer.analyze_patterns(incidents, window_start, window_end);
68 analyses.push((window_days, analysis));
69 }
70
71 let (_, analysis) =
73 analyses.iter().max_by_key(|(days, _)| *days).or_else(|| analyses.first())?;
74
75 let change_probability = self
77 .statistical_model
78 .predict_change_probability(analysis, forecast_window_days);
79 let break_probability =
80 self.statistical_model.predict_break_probability(analysis, forecast_window_days);
81 let next_change_date = self.statistical_model.predict_next_change_date(analysis);
82 let next_break_date = self.statistical_model.predict_next_break_date(analysis);
83 let confidence = self
84 .statistical_model
85 .calculate_confidence(analysis, self.config.min_incidents_for_forecast);
86
87 let seasonal_patterns: Vec<_> = analysis
89 .patterns
90 .iter()
91 .filter(|p| {
92 matches!(
93 p.pattern_type,
94 super::types::PatternType::MonthlyMaintenance
95 | super::types::PatternType::QuarterlyRefactor
96 | super::types::PatternType::WeeklyUpdate
97 )
98 })
99 .map(|p| super::types::SeasonalPattern {
100 pattern_type: p.pattern_type.clone(),
101 frequency_days: p.frequency_days,
102 last_occurrence: p.last_occurrence,
103 confidence: p.confidence,
104 description: format!("{:?} pattern", p.pattern_type),
105 })
106 .collect();
107
108 let expires_at = Utc::now() + Duration::hours(self.config.default_expiration_hours as i64);
109
110 Some(ChangeForecast {
111 service_id,
112 service_name,
113 endpoint,
114 method,
115 forecast_window_days,
116 predicted_change_probability: change_probability,
117 predicted_break_probability: break_probability,
118 next_expected_change_date: next_change_date,
119 next_expected_break_date: next_break_date,
120 volatility_score: analysis.volatility_score,
121 confidence,
122 seasonal_patterns,
123 predicted_at: Utc::now(),
124 expires_at,
125 })
126 }
127
128 pub fn analyze_historical_patterns(
130 &self,
131 incidents: &[DriftIncident],
132 window_start: DateTime<Utc>,
133 window_end: DateTime<Utc>,
134 ) -> PatternAnalysis {
135 self.pattern_analyzer.analyze_patterns(incidents, window_start, window_end)
136 }
137}
138
139impl Default for Forecaster {
140 fn default() -> Self {
141 Self::new(ForecastingConfig::default())
142 }
143}