Skip to main content

wm_selfmodel/
lib.rs

1//! wm-selfmodel — Predictive introspection for WhiteMagic v5 (Phase R4).
2//!
3//! Tracks per-subsystem metrics over time, forecasts threshold crossings,
4//! and feeds confidence signals back into the dispatch pipeline.
5//!
6//! Architecture:
7//! - [`MetricTracker`] — per-metric ring buffer history with EWMA
8//! - [`ForecastEngine`] — linear extrapolation + EWMA forecasting
9//! - [`AlertEngine`] — threshold rules checked against forecasts
10//! - [`ConfidenceCalibrator`] — overall system confidence from forecast accuracy
11//! - [`SelfModel`] — top-level orchestrator
12//!
13//! The self-model is read-only from the dispatch pipeline (no feedback loops).
14//! Confidence <0.5 triggers conservative dispatch (prefer cached results).
15
16#![forbid(unsafe_code)]
17#![allow(clippy::significant_drop_tightening)]
18
19pub mod alert;
20pub mod confidence;
21pub mod forecast;
22pub mod metrics;
23
24pub use alert::{Alert, AlertEngine, AlertLevel, AlertRule, Comparison};
25pub use confidence::ConfidenceCalibrator;
26pub use forecast::{Forecast, ForecastEngine};
27pub use metrics::{MetricKind, MetricSample, MetricTracker};
28
29use chrono::{DateTime, Utc};
30use serde::{Deserialize, Serialize};
31use std::collections::VecDeque;
32use std::sync::RwLock;
33
34/// Cognitive metrics bundle — recorded after each imagination/research cycle.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CognitiveMetrics {
37    /// Imagination quality score (0.0–1.0).
38    pub imagination_quality: f32,
39    /// Research output rate (0.0–1.0).
40    pub research_output: f32,
41    /// Scenario confidence from MC rollout (0.0–1.0).
42    pub scenario_confidence: f32,
43    /// Simulation variance (0.0–1.0, lower is better).
44    pub simulation_variance: f32,
45}
46
47/// Forecast for all cognitive metrics.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct CognitiveForecast {
50    /// Forecast for imagination quality.
51    pub imagination_quality: Option<Forecast>,
52    /// Forecast for research output.
53    pub research_output: Option<Forecast>,
54    /// Forecast for scenario confidence.
55    pub scenario_confidence: Option<Forecast>,
56    /// Forecast for simulation variance.
57    pub simulation_variance: Option<Forecast>,
58}
59
60impl CognitiveForecast {
61    /// Whether all cognitive forecasts are available.
62    #[must_use]
63    pub const fn is_complete(&self) -> bool {
64        self.imagination_quality.is_some()
65            && self.research_output.is_some()
66            && self.scenario_confidence.is_some()
67            && self.simulation_variance.is_some()
68    }
69
70    /// Convert to JSON.
71    #[must_use]
72    pub fn to_json(&self) -> serde_json::Value {
73        serde_json::json!({
74            "imagination_quality": self.imagination_quality.as_ref().map(|f| f.predicted_value),
75            "research_output": self.research_output.as_ref().map(|f| f.predicted_value),
76            "scenario_confidence": self.scenario_confidence.as_ref().map(|f| f.predicted_value),
77            "simulation_variance": self.simulation_variance.as_ref().map(|f| f.predicted_value),
78        })
79    }
80}
81
82/// Maximum number of historical samples kept per metric.
83const DEFAULT_HISTORY_CAPACITY: usize = 256;
84
85/// Top-level self-model — orchestrates metric tracking, forecasting,
86/// alerting, and confidence calibration.
87pub struct SelfModel {
88    metrics: RwLock<MetricTracker>,
89    forecast_engine: ForecastEngine,
90    alert_engine: RwLock<AlertEngine>,
91    calibrator: RwLock<ConfidenceCalibrator>,
92}
93
94impl SelfModel {
95    /// Create a new self-model with default history capacity and alert rules.
96    #[must_use]
97    pub fn new() -> Self {
98        Self::with_capacity(DEFAULT_HISTORY_CAPACITY)
99    }
100
101    /// Create a new self-model with the given history capacity per metric.
102    #[must_use]
103    pub fn with_capacity(capacity: usize) -> Self {
104        Self {
105            metrics: RwLock::new(MetricTracker::new(capacity)),
106            forecast_engine: ForecastEngine::new(),
107            alert_engine: RwLock::new(AlertEngine::with_default_rules()),
108            calibrator: RwLock::new(ConfidenceCalibrator::new()),
109        }
110    }
111
112    /// Record a metric sample.
113    pub fn record(&self, kind: MetricKind, value: f32) {
114        let sample = MetricSample {
115            kind,
116            value,
117            timestamp: Utc::now(),
118        };
119        if let Ok(mut metrics) = self.metrics.write() {
120            metrics.record(sample);
121        }
122    }
123
124    /// Record a metric sample with an explicit timestamp (for testing/replay).
125    pub fn record_at(&self, kind: MetricKind, value: f32, timestamp: DateTime<Utc>) {
126        let sample = MetricSample {
127            kind,
128            value,
129            timestamp,
130        };
131        if let Ok(mut metrics) = self.metrics.write() {
132            metrics.record(sample);
133        }
134    }
135
136    /// Forecast a metric `horizon` samples into the future.
137    #[must_use]
138    pub fn forecast(&self, kind: MetricKind, horizon: usize) -> Option<Forecast> {
139        let history = {
140            let metrics = self.metrics.read().ok()?;
141            let history = metrics.history(kind)?;
142            if history.len() < 2 {
143                return None;
144            }
145            history.clone()
146        };
147        Some(self.forecast_engine.forecast(&history, horizon))
148    }
149
150    /// Forecast all tracked metrics.
151    #[must_use]
152    pub fn forecast_all(&self, horizon: usize) -> Vec<(MetricKind, Forecast)> {
153        let histories: Vec<(MetricKind, VecDeque<MetricSample>)> = {
154            let metrics = self.metrics.read();
155            let Ok(metrics) = metrics else {
156                return Vec::new();
157            };
158            metrics
159                .tracked_kinds()
160                .filter_map(|kind| {
161                    let history = metrics.history(kind)?;
162                    if history.len() < 2 {
163                        return None;
164                    }
165                    Some((kind, history.clone()))
166                })
167                .collect()
168        };
169        histories
170            .into_iter()
171            .map(|(kind, history)| (kind, self.forecast_engine.forecast(&history, horizon)))
172            .collect()
173    }
174
175    /// Check all alert rules against current forecasts.
176    #[must_use]
177    pub fn check_alerts(&self) -> Vec<Alert> {
178        // Extract histories and rules while holding locks, then release
179        let histories: Vec<(MetricKind, VecDeque<MetricSample>)> = {
180            let metrics = self.metrics.read();
181            let Ok(metrics) = metrics else {
182                return Vec::new();
183            };
184            metrics
185                .tracked_kinds()
186                .filter_map(|kind| {
187                    let history = metrics.history(kind)?;
188                    if history.len() < 2 {
189                        return None;
190                    }
191                    Some((kind, history.clone()))
192                })
193                .collect()
194        };
195
196        let rules: Vec<AlertRule> = {
197            let alert_engine = self.alert_engine.read();
198            let Ok(alert_engine) = alert_engine else {
199                return Vec::new();
200            };
201            alert_engine.rules().to_vec()
202        };
203
204        let mut alerts = Vec::new();
205        for rule in &rules {
206            if let Some((_, history)) = histories.iter().find(|(k, _)| *k == rule.metric) {
207                let forecast = self.forecast_engine.forecast(history, rule.horizon);
208                if let Some(alert) = AlertEngine::evaluate_rule(rule, &forecast) {
209                    alerts.push(alert);
210                }
211            }
212        }
213        alerts
214    }
215
216    /// Get the overall system confidence (0.0–1.0).
217    /// Confidence <0.5 triggers conservative dispatch.
218    #[must_use]
219    pub fn confidence(&self) -> f32 {
220        let (current, accuracy) = {
221            let metrics = self.metrics.read();
222            let Ok(metrics) = metrics else {
223                return 0.5;
224            };
225
226            // Get current values for all tracked metrics
227            let current: Vec<(MetricKind, f32)> = metrics
228                .tracked_kinds()
229                .filter_map(|kind| metrics.latest(kind).map(|s| (kind, s.value)))
230                .collect();
231
232            if current.is_empty() {
233                return 0.5;
234            }
235
236            let accuracy = self.compute_forecast_accuracy(&metrics);
237            (current, accuracy)
238        };
239
240        if let Ok(mut calibrator) = self.calibrator.write() {
241            calibrator.update(&current, accuracy);
242            calibrator.confidence()
243        } else {
244            0.5
245        }
246    }
247
248    /// Compute forecast accuracy by comparing past forecasts to actual values.
249    fn compute_forecast_accuracy(&self, metrics: &MetricTracker) -> f32 {
250        let mut total_error = 0.0_f32;
251        let mut count = 0_u32;
252
253        for kind in metrics.tracked_kinds() {
254            let history = match metrics.history(kind) {
255                Some(h) if h.len() >= 4 => h,
256                _ => continue,
257            };
258
259            // Compare forecast from t-2 to actual at t-1
260            let past: VecDeque<MetricSample> =
261                history.iter().take(history.len() - 1).cloned().collect();
262            let actual = history.back().unwrap().value;
263
264            let forecast = self.forecast_engine.forecast(&past, 1);
265            let error = (forecast.predicted_value - actual).abs() / actual.max(0.001);
266            total_error += error.min(1.0);
267            count += 1;
268        }
269
270        if count == 0 {
271            return 0.5; // Unknown accuracy
272        }
273
274        let avg_error = total_error / count as f32;
275        (1.0 - avg_error).clamp(0.0, 1.0)
276    }
277
278    /// Take a snapshot of the entire self-model state.
279    #[must_use]
280    pub fn snapshot(&self) -> SelfModelSnapshot {
281        // Extract all data from metrics lock first
282        let (metric_snapshots, histories): (
283            Vec<MetricSnapshot>,
284            Vec<(MetricKind, VecDeque<MetricSample>)>,
285        ) = {
286            let metrics = self.metrics.read();
287            let Ok(metrics) = metrics else {
288                return SelfModelSnapshot {
289                    timestamp: Utc::now(),
290                    confidence: 0.5,
291                    metrics: Vec::new(),
292                    alerts: Vec::new(),
293                    forecasts: Vec::new(),
294                };
295            };
296
297            let histories: Vec<(MetricKind, VecDeque<MetricSample>)> = metrics
298                .tracked_kinds()
299                .filter_map(|kind| {
300                    let history = metrics.history(kind)?;
301                    if history.len() < 2 {
302                        return None;
303                    }
304                    Some((kind, history.clone()))
305                })
306                .collect();
307
308            let metric_snapshots: Vec<MetricSnapshot> = metrics
309                .tracked_kinds()
310                .filter_map(|kind| {
311                    let latest = metrics.latest(kind)?;
312                    let hist = metrics.history(kind)?;
313                    let values: Vec<f32> = hist.iter().map(|s| s.value).collect();
314                    Some(MetricSnapshot {
315                        kind,
316                        current: latest.value,
317                        min: values.iter().copied().fold(f32::INFINITY, f32::min),
318                        max: values.iter().copied().fold(f32::NEG_INFINITY, f32::max),
319                        avg: values.iter().copied().sum::<f32>() / values.len() as f32,
320                        sample_count: values.len(),
321                    })
322                })
323                .collect();
324
325            (metric_snapshots, histories)
326        };
327
328        // Now compute forecasts without holding the lock
329        let forecasts: Vec<(MetricKind, Forecast)> = histories
330            .iter()
331            .map(|(kind, history)| (*kind, self.forecast_engine.forecast(history, 5)))
332            .collect();
333
334        let confidence = self.confidence();
335        let alerts = self.check_alerts();
336
337        SelfModelSnapshot {
338            timestamp: Utc::now(),
339            confidence,
340            metrics: metric_snapshots,
341            alerts,
342            forecasts,
343        }
344    }
345
346    /// Add a custom alert rule.
347    pub fn add_alert_rule(&self, rule: AlertRule) {
348        if let Ok(mut engine) = self.alert_engine.write() {
349            engine.add_rule(rule);
350        }
351    }
352
353    /// Get the number of tracked metrics.
354    #[must_use]
355    pub fn tracked_count(&self) -> usize {
356        self.metrics.read().map_or(0, |m| m.tracked_count())
357    }
358
359    /// Get the number of samples for a specific metric.
360    #[must_use]
361    pub fn sample_count(&self, kind: MetricKind) -> usize {
362        self.metrics.read().map_or(0, |m| m.sample_count(kind))
363    }
364
365    /// Record cognitive metrics from an imagination/research cycle.
366    ///
367    /// Convenience method that records all four cognitive metrics at once.
368    pub fn record_cognitive(&self, metrics: &CognitiveMetrics) {
369        self.record(MetricKind::ImaginationQuality, metrics.imagination_quality);
370        self.record(MetricKind::ResearchOutput, metrics.research_output);
371        self.record(MetricKind::ScenarioConfidence, metrics.scenario_confidence);
372        self.record(MetricKind::SimulationVariance, metrics.simulation_variance);
373    }
374
375    /// Record cognitive metrics with an explicit timestamp (for testing/replay).
376    pub fn record_cognitive_at(&self, metrics: &CognitiveMetrics, timestamp: DateTime<Utc>) {
377        self.record_at(
378            MetricKind::ImaginationQuality,
379            metrics.imagination_quality,
380            timestamp,
381        );
382        self.record_at(
383            MetricKind::ResearchOutput,
384            metrics.research_output,
385            timestamp,
386        );
387        self.record_at(
388            MetricKind::ScenarioConfidence,
389            metrics.scenario_confidence,
390            timestamp,
391        );
392        self.record_at(
393            MetricKind::SimulationVariance,
394            metrics.simulation_variance,
395            timestamp,
396        );
397    }
398
399    /// Forecast cognitive metrics `horizon` samples ahead.
400    ///
401    /// Returns forecasts for all four cognitive metrics that have enough history.
402    #[must_use]
403    pub fn forecast_cognitive(&self, horizon: usize) -> CognitiveForecast {
404        CognitiveForecast {
405            imagination_quality: self.forecast(MetricKind::ImaginationQuality, horizon),
406            research_output: self.forecast(MetricKind::ResearchOutput, horizon),
407            scenario_confidence: self.forecast(MetricKind::ScenarioConfidence, horizon),
408            simulation_variance: self.forecast(MetricKind::SimulationVariance, horizon),
409        }
410    }
411
412    /// Check cognitive-specific alert rules.
413    ///
414    /// Returns alerts for cognitive metrics only (imagination quality,
415    /// research output, scenario confidence, simulation variance).
416    #[must_use]
417    pub fn check_cognitive_alerts(&self) -> Vec<Alert> {
418        self.check_alerts()
419            .into_iter()
420            .filter(|a| {
421                matches!(
422                    a.metric,
423                    MetricKind::ImaginationQuality
424                        | MetricKind::ResearchOutput
425                        | MetricKind::ScenarioConfidence
426                        | MetricKind::SimulationVariance
427                )
428            })
429            .collect()
430    }
431
432    /// Serialize the full self-model state for persistence.
433    ///
434    /// Includes per-metric histories (with timestamps), alert rules, and
435    /// the confidence calibrator state, so a restarted process resumes
436    /// forecasting, drift alerts, and confidence exactly where it left off.
437    #[must_use]
438    pub fn to_json(&self) -> serde_json::Value {
439        let (samples, rules, last_confidence, smoothing) = {
440            let samples = {
441                let metrics = self.metrics.read();
442                let Ok(metrics) = metrics else {
443                    return serde_json::json!({});
444                };
445                metrics
446                    .tracked_kinds()
447                    .filter_map(|kind| {
448                        metrics
449                            .history(kind)
450                            .cloned()
451                            .map(|h| h.into_iter().collect::<Vec<_>>())
452                    })
453                    .flatten()
454                    .collect::<Vec<MetricSample>>()
455            };
456            let rules = {
457                let engine = self.alert_engine.read();
458                engine.map(|e| e.rules().to_vec()).unwrap_or_default()
459            };
460            let calibrator = {
461                let c = self.calibrator.read();
462                c.map_or((0.5, 0.2), |c| (c.state().0, c.state().1))
463            };
464            (samples, rules, calibrator.0, calibrator.1)
465        };
466        serde_json::json!({
467            "samples": samples,
468            "rules": rules,
469            "last_confidence": last_confidence,
470            "smoothing": smoothing,
471        })
472    }
473
474    /// Restore self-model state from previously persisted JSON.
475    ///
476    /// Unknown or malformed fields are skipped; partial state restores
477    /// gracefully. Returns an error only if the top-level shape is wrong.
478    pub fn from_json(&self, value: &serde_json::Value) -> Result<(), String> {
479        let samples = value
480            .get("samples")
481            .and_then(serde_json::Value::as_array)
482            .ok_or_else(|| "self-model state missing 'samples'".to_string())?;
483        let restored: Vec<MetricSample> = samples
484            .iter()
485            .filter_map(|s| serde_json::from_value(s.clone()).ok())
486            .collect();
487        {
488            let mut metrics = self
489                .metrics
490                .write()
491                .map_err(|e| format!("metrics lock: {e}"))?;
492            for sample in restored {
493                metrics.record(sample);
494            }
495        }
496        if let Some(rules) = value.get("rules").and_then(serde_json::Value::as_array) {
497            let rules: Vec<AlertRule> = rules
498                .iter()
499                .filter_map(|r| serde_json::from_value(r.clone()).ok())
500                .collect();
501            if let Ok(mut engine) = self.alert_engine.write() {
502                for rule in rules {
503                    engine.add_rule(rule);
504                }
505            }
506        }
507        if let Some(confidence) = value
508            .get("last_confidence")
509            .and_then(serde_json::Value::as_f64)
510        {
511            let smoothing = value
512                .get("smoothing")
513                .and_then(serde_json::Value::as_f64)
514                .unwrap_or(0.2);
515            if let Ok(mut calibrator) = self.calibrator.write() {
516                calibrator.restore_state(confidence as f32, smoothing as f32);
517            }
518        }
519        Ok(())
520    }
521}
522
523impl Default for SelfModel {
524    fn default() -> Self {
525        Self::new()
526    }
527}
528
529/// A point-in-time snapshot of the self-model state.
530#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct SelfModelSnapshot {
532    /// When the snapshot was taken.
533    pub timestamp: DateTime<Utc>,
534    /// Overall system confidence (0.0–1.0).
535    pub confidence: f32,
536    /// Per-metric summaries.
537    pub metrics: Vec<MetricSnapshot>,
538    /// Active alerts.
539    pub alerts: Vec<Alert>,
540    /// Forecasts for all tracked metrics (5 samples ahead).
541    pub forecasts: Vec<(MetricKind, Forecast)>,
542}
543
544/// Summary of a single metric's state.
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct MetricSnapshot {
547    /// Which metric.
548    pub kind: MetricKind,
549    /// Current (most recent) value.
550    pub current: f32,
551    /// Minimum value in history.
552    pub min: f32,
553    /// Maximum value in history.
554    pub max: f32,
555    /// Average value across all samples.
556    pub avg: f32,
557    /// Number of samples.
558    pub sample_count: usize,
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564
565    #[test]
566    fn self_model_record_and_forecast() {
567        let model = SelfModel::new();
568        model.record(MetricKind::CpuLoad, 0.3);
569        model.record(MetricKind::CpuLoad, 0.4);
570        model.record(MetricKind::CpuLoad, 0.5);
571
572        let forecast = model.forecast(MetricKind::CpuLoad, 3);
573        assert!(forecast.is_some());
574        let f = forecast.unwrap();
575        assert!(f.predicted_value > 0.4);
576        assert!(f.confidence > 0.0);
577    }
578
579    #[test]
580    fn self_model_insufficient_data_returns_none() {
581        let model = SelfModel::new();
582        model.record(MetricKind::CpuLoad, 0.3);
583
584        assert!(model.forecast(MetricKind::CpuLoad, 3).is_none());
585    }
586
587    #[test]
588    fn self_model_confidence_no_data() {
589        let model = SelfModel::new();
590        let conf = model.confidence();
591        assert_eq!(conf, 0.5);
592    }
593
594    #[test]
595    fn self_model_confidence_with_data() {
596        let model = SelfModel::new();
597        for v in [0.1, 0.15, 0.12, 0.13, 0.11, 0.14] {
598            model.record(MetricKind::CpuLoad, v);
599        }
600        let conf = model.confidence();
601        assert!(conf > 0.0 && conf <= 1.0);
602    }
603
604    #[test]
605    fn self_model_snapshot_empty() {
606        let model = SelfModel::new();
607        let snap = model.snapshot();
608        assert_eq!(snap.metrics.len(), 0);
609        assert_eq!(snap.alerts.len(), 0);
610        assert_eq!(snap.forecasts.len(), 0);
611    }
612
613    #[test]
614    fn self_model_json_roundtrip_preserves_histories() {
615        let model = SelfModel::new();
616        model.record(MetricKind::ConformalCoverage, 0.9);
617        model.record(MetricKind::ConformalCoverage, 0.82);
618        model.record(MetricKind::BrierScore, 0.2);
619        model.record_at(
620            MetricKind::Coherence,
621            0.7,
622            chrono::DateTime::parse_from_rfc3339("2026-08-09T00:00:00Z")
623                .unwrap()
624                .with_timezone(&chrono::Utc),
625        );
626
627        let json = model.to_json();
628        assert!(json["samples"].as_array().unwrap().len() >= 3);
629
630        let restored = SelfModel::new();
631        restored.from_json(&json).unwrap();
632        assert_eq!(restored.sample_count(MetricKind::ConformalCoverage), 2);
633        assert_eq!(restored.sample_count(MetricKind::BrierScore), 1);
634        assert_eq!(restored.sample_count(MetricKind::Coherence), 1);
635
636        // Histories are usable for forecasting after restore.
637        assert!(
638            restored
639                .forecast(MetricKind::ConformalCoverage, 3)
640                .is_some()
641        );
642        // Alert rules restored with defaults still fire on drift.
643        restored.record(MetricKind::ConformalCoverage, 0.75);
644        restored.record(MetricKind::ConformalCoverage, 0.72);
645        let drift = restored
646            .check_alerts()
647            .into_iter()
648            .any(|a| a.metric == MetricKind::ConformalCoverage);
649        assert!(drift);
650    }
651
652    #[test]
653    fn self_model_json_roundtrip_preserves_calibrator() {
654        let model = SelfModel::new();
655        model.record(MetricKind::CpuLoad, 0.3);
656        let json = model.to_json();
657        let restored = SelfModel::new();
658        restored.from_json(&json).unwrap();
659        // Confidence is derived live from metric values — a restored model
660        // with the same history computes the same confidence as a fresh one.
661        let fresh = SelfModel::new();
662        fresh.record(MetricKind::CpuLoad, 0.3);
663        let (a, b) = (restored.confidence(), fresh.confidence());
664        assert!((a - b).abs() < 1e-6, "restored {a} != fresh {b}");
665    }
666
667    #[test]
668    fn self_model_from_json_malformed_errors() {
669        let model = SelfModel::new();
670        let err = model.from_json(&serde_json::json!({"nope": true}));
671        assert!(err.is_err());
672    }
673
674    #[test]
675    fn self_model_snapshot_with_data() {
676        let model = SelfModel::new();
677        model.record(MetricKind::CpuLoad, 0.3);
678        model.record(MetricKind::CpuLoad, 0.5);
679        model.record(MetricKind::CpuLoad, 0.7);
680        model.record(MetricKind::MemoryPressure, 0.2);
681        model.record(MetricKind::MemoryPressure, 0.3);
682
683        let snap = model.snapshot();
684        assert_eq!(snap.metrics.len(), 2);
685        assert!(snap.metrics.iter().any(|m| m.kind == MetricKind::CpuLoad));
686        assert!(
687            snap.metrics
688                .iter()
689                .any(|m| m.kind == MetricKind::MemoryPressure)
690        );
691        assert_eq!(snap.forecasts.len(), 2);
692    }
693
694    #[test]
695    fn self_model_check_alerts_clear() {
696        let model = SelfModel::new();
697        for v in [0.1, 0.12, 0.11, 0.13, 0.12] {
698            model.record(MetricKind::CpuLoad, v);
699        }
700        let alerts = model.check_alerts();
701        // CPU load trending up slightly but shouldn't trigger critical alert
702        assert!(!alerts.iter().any(|a| a.level == AlertLevel::Critical));
703    }
704
705    #[test]
706    fn self_model_check_alerts_triggered() {
707        let model = SelfModel::new();
708        // CPU load trending toward 1.0 — should trigger warning/critical
709        for v in [0.5, 0.6, 0.7, 0.8, 0.9, 0.95] {
710            model.record(MetricKind::CpuLoad, v);
711        }
712        let alerts = model.check_alerts();
713        assert!(!alerts.is_empty());
714        assert!(alerts.iter().any(|a| a.metric == MetricKind::CpuLoad));
715    }
716
717    #[test]
718    fn self_model_tracked_count() {
719        let model = SelfModel::new();
720        model.record(MetricKind::CpuLoad, 0.3);
721        model.record(MetricKind::MemoryPressure, 0.2);
722        assert_eq!(model.tracked_count(), 2);
723    }
724
725    #[test]
726    fn self_model_sample_count() {
727        let model = SelfModel::new();
728        model.record(MetricKind::CpuLoad, 0.3);
729        model.record(MetricKind::CpuLoad, 0.4);
730        model.record(MetricKind::CpuLoad, 0.5);
731        assert_eq!(model.sample_count(MetricKind::CpuLoad), 3);
732        assert_eq!(model.sample_count(MetricKind::MemoryPressure), 0);
733    }
734
735    #[test]
736    fn self_model_add_custom_alert_rule() {
737        let model = SelfModel::new();
738        model.add_alert_rule(AlertRule {
739            metric: MetricKind::ErrorRate,
740            threshold: 0.1,
741            comparison: Comparison::GreaterThan,
742            horizon: 3,
743            level: AlertLevel::Critical,
744        });
745        for v in [0.01, 0.02, 0.01, 0.02] {
746            model.record(MetricKind::ErrorRate, v);
747        }
748        // Error rate is low, should not trigger
749        let alerts = model.check_alerts();
750        assert!(!alerts.iter().any(|a| a.metric == MetricKind::ErrorRate));
751    }
752
753    #[test]
754    fn self_model_forecast_all() {
755        let model = SelfModel::new();
756        model.record(MetricKind::CpuLoad, 0.3);
757        model.record(MetricKind::CpuLoad, 0.4);
758        model.record(MetricKind::MemoryPressure, 0.2);
759        model.record(MetricKind::MemoryPressure, 0.25);
760
761        let forecasts = model.forecast_all(5);
762        assert_eq!(forecasts.len(), 2);
763    }
764
765    #[test]
766    fn self_model_default_impl() {
767        let model = SelfModel::default();
768        assert_eq!(model.tracked_count(), 0);
769    }
770
771    #[test]
772    fn self_model_snapshot_serialization() {
773        let model = SelfModel::new();
774        model.record(MetricKind::CpuLoad, 0.3);
775        model.record(MetricKind::CpuLoad, 0.5);
776        let snap = model.snapshot();
777        let json = serde_json::to_string(&snap).unwrap();
778        let back: SelfModelSnapshot = serde_json::from_str(&json).unwrap();
779        assert!((back.confidence - snap.confidence).abs() < 0.01);
780    }
781
782    // ── Cognitive metrics tests ──
783
784    #[test]
785    fn record_cognitive_records_all_four_metrics() {
786        let model = SelfModel::new();
787        let cm = CognitiveMetrics {
788            imagination_quality: 0.7,
789            research_output: 0.5,
790            scenario_confidence: 0.8,
791            simulation_variance: 0.1,
792        };
793        model.record_cognitive(&cm);
794        assert_eq!(model.sample_count(MetricKind::ImaginationQuality), 1);
795        assert_eq!(model.sample_count(MetricKind::ResearchOutput), 1);
796        assert_eq!(model.sample_count(MetricKind::ScenarioConfidence), 1);
797        assert_eq!(model.sample_count(MetricKind::SimulationVariance), 1);
798    }
799
800    #[test]
801    fn forecast_cognitive_returns_forecasts() {
802        let model = SelfModel::new();
803        for i in 0..5 {
804            let cm = CognitiveMetrics {
805                imagination_quality: 0.5_f32.mul_add(i as f32 * 0.05, 0.0),
806                research_output: 0.3_f32.mul_add(i as f32 * 0.02, 0.0),
807                scenario_confidence: 0.6,
808                simulation_variance: 0.15,
809            };
810            model.record_cognitive(&cm);
811        }
812        let forecast = model.forecast_cognitive(3);
813        assert!(forecast.imagination_quality.is_some());
814        assert!(forecast.research_output.is_some());
815        assert!(forecast.scenario_confidence.is_some());
816        assert!(forecast.simulation_variance.is_some());
817        assert!(forecast.is_complete());
818    }
819
820    #[test]
821    fn forecast_cognitive_insufficient_data() {
822        let model = SelfModel::new();
823        let cm = CognitiveMetrics {
824            imagination_quality: 0.5,
825            research_output: 0.3,
826            scenario_confidence: 0.6,
827            simulation_variance: 0.15,
828        };
829        model.record_cognitive(&cm);
830        let forecast = model.forecast_cognitive(3);
831        // Only 1 sample — not enough for forecast
832        assert!(!forecast.is_complete());
833    }
834
835    #[test]
836    fn check_cognitive_alerts_filters_cognitive_only() {
837        let model = SelfModel::new();
838        // Record declining imagination quality (should trigger warning)
839        for v in [0.7, 0.6, 0.5, 0.4, 0.3, 0.2] {
840            model.record(MetricKind::ImaginationQuality, v);
841        }
842        // Also record CPU load (should not appear in cognitive alerts)
843        for v in [0.1, 0.12, 0.11, 0.13, 0.12] {
844            model.record(MetricKind::CpuLoad, v);
845        }
846        let cognitive_alerts = model.check_cognitive_alerts();
847        assert!(!cognitive_alerts.is_empty());
848        // All alerts should be for cognitive metrics only
849        assert!(cognitive_alerts.iter().all(|a| {
850            matches!(
851                a.metric,
852                MetricKind::ImaginationQuality
853                    | MetricKind::ResearchOutput
854                    | MetricKind::ScenarioConfidence
855                    | MetricKind::SimulationVariance
856            )
857        }));
858    }
859
860    #[test]
861    fn cognitive_metrics_serialization() {
862        let cm = CognitiveMetrics {
863            imagination_quality: 0.7,
864            research_output: 0.5,
865            scenario_confidence: 0.8,
866            simulation_variance: 0.1,
867        };
868        let json = serde_json::to_string(&cm).unwrap();
869        let back: CognitiveMetrics = serde_json::from_str(&json).unwrap();
870        assert!((back.imagination_quality - 0.7).abs() < 0.001);
871        assert!((back.research_output - 0.5).abs() < 0.001);
872    }
873
874    #[test]
875    fn cognitive_forecast_to_json() {
876        let model = SelfModel::new();
877        for i in 0..3 {
878            let cm = CognitiveMetrics {
879                imagination_quality: 0.5_f32.mul_add(i as f32 * 0.1, 0.0),
880                research_output: 0.3,
881                scenario_confidence: 0.6,
882                simulation_variance: 0.15,
883            };
884            model.record_cognitive(&cm);
885        }
886        let forecast = model.forecast_cognitive(2);
887        let json = forecast.to_json();
888        assert!(json["imagination_quality"].as_f64().is_some());
889    }
890
891    #[test]
892    fn simulation_variance_higher_is_better_false() {
893        assert!(!MetricKind::SimulationVariance.higher_is_better());
894    }
895
896    #[test]
897    fn imagination_quality_higher_is_better_true() {
898        assert!(MetricKind::ImaginationQuality.higher_is_better());
899        assert!(MetricKind::ResearchOutput.higher_is_better());
900        assert!(MetricKind::ScenarioConfidence.higher_is_better());
901    }
902
903    #[test]
904    fn cognitive_metric_thresholds() {
905        assert_eq!(MetricKind::ImaginationQuality.default_warning(), 0.4);
906        assert_eq!(MetricKind::ImaginationQuality.default_critical(), 0.2);
907        assert_eq!(MetricKind::SimulationVariance.default_warning(), 0.3);
908        assert_eq!(MetricKind::SimulationVariance.default_critical(), 0.5);
909    }
910
911    #[test]
912    fn record_cognitive_at_with_timestamp() {
913        let model = SelfModel::new();
914        let ts = Utc::now();
915        let cm = CognitiveMetrics {
916            imagination_quality: 0.7,
917            research_output: 0.5,
918            scenario_confidence: 0.8,
919            simulation_variance: 0.1,
920        };
921        model.record_cognitive_at(&cm, ts);
922        assert_eq!(model.sample_count(MetricKind::ImaginationQuality), 1);
923    }
924}