Skip to main content

mockforge_contracts/contract_drift/forecasting/
forecaster.rs

1//! Main forecasting engine
2//!
3//! This module orchestrates pattern analysis and statistical modeling
4//! to generate change forecasts.
5
6use 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
12/// Main forecaster for API change predictions
13pub struct Forecaster {
14    /// Pattern analyzer
15    pattern_analyzer: PatternAnalyzer,
16    /// Statistical model
17    statistical_model: StatisticalModel,
18    /// Configuration
19    config: ForecastingConfig,
20}
21
22impl Forecaster {
23    /// Create a new forecaster
24    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    /// Generate forecast for a service or endpoint
39    #[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        // Analyze patterns for each time window
59        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        // Use the longest window analysis for forecasting
72        let (_, analysis) =
73            analyses.iter().max_by_key(|(days, _)| *days).or_else(|| analyses.first())?;
74
75        // Generate predictions
76        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        // Extract seasonal patterns
88        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    /// Analyze historical patterns (for statistics generation)
129    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}