Skip to main content

clawft_kernel/
eml_persistence.rs

1//! EML model persistence — save/load all 12+ EML models to `.weftos/eml-models/`.
2//!
3//! Provides [`save_all`] and [`load_all`] functions used during kernel
4//! shutdown and boot respectively. Models are stored as JSON files for
5//! portability and debuggability.
6//!
7//! # Model Inventory
8//!
9//! | File | Source Crate | Model |
10//! |------|-------------|-------|
11//! | `coherence.json` | clawft-kernel | EmlCoherenceModel |
12//! | `governance_scorer.json` | clawft-kernel | GovernanceScorerModel |
13//! | `restart_strategy.json` | clawft-kernel | RestartStrategyModel |
14//! | `health_threshold.json` | clawft-kernel | HealthThresholdModel |
15//! | `dead_letter.json` | clawft-kernel | DeadLetterModel |
16//! | `gossip_timing.json` | clawft-kernel | GossipTimingModel |
17//! | `complexity.json` | clawft-kernel | ComplexityModel |
18//! | `hnsw_distance.json` | clawft-kernel | HnswEmlManager (distance) |
19//! | `hnsw_ef.json` | clawft-kernel | HnswEmlManager (ef) |
20//! | `hnsw_path.json` | clawft-kernel | HnswEmlManager (path) |
21//! | `hnsw_rebuild.json` | clawft-kernel | HnswEmlManager (rebuild) |
22
23use std::path::{Path, PathBuf};
24
25use tracing::{debug, info, warn};
26
27use crate::eml_coherence::EmlCoherenceModel;
28use crate::eml_kernel::{
29    ComplexityModel, DeadLetterModel, GossipTimingModel, GovernanceScorerModel,
30    HealthThresholdModel, RestartStrategyModel,
31};
32
33// ---------------------------------------------------------------------------
34// Directory
35// ---------------------------------------------------------------------------
36
37/// Default persistence directory: `.weftos/eml-models/`
38///
39/// Located relative to the runtime directory (typically the project root
40/// or `$WEFTOS_RUNTIME_DIR`).
41pub fn eml_models_dir(runtime_dir: &Path) -> PathBuf {
42    runtime_dir.join(".weftos").join("eml-models")
43}
44
45// ---------------------------------------------------------------------------
46// Kernel EML Models Bundle
47// ---------------------------------------------------------------------------
48
49/// Bundle of all kernel-level EML models for save/load.
50///
51/// This does NOT include graphify, LLM, or benchmark models -- those
52/// are persisted by their respective crates. This bundle covers the 7
53/// models owned by `clawft-kernel`.
54#[derive(Default)]
55pub struct KernelEmlModels {
56    pub coherence: EmlCoherenceModel,
57    pub governance_scorer: GovernanceScorerModel,
58    pub restart_strategy: RestartStrategyModel,
59    pub health_threshold: HealthThresholdModel,
60    pub dead_letter: DeadLetterModel,
61    pub gossip_timing: GossipTimingModel,
62    pub complexity: ComplexityModel,
63}
64
65// ---------------------------------------------------------------------------
66// Save/Load helpers
67// ---------------------------------------------------------------------------
68
69/// Save a single serde-serializable model to a JSON file.
70fn save_model<T: serde::Serialize>(dir: &Path, filename: &str, model: &T) -> std::io::Result<()> {
71    let path = dir.join(filename);
72    let json = serde_json::to_string_pretty(model)
73        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
74    std::fs::write(&path, json)?;
75    debug!(path = %path.display(), "saved EML model");
76    Ok(())
77}
78
79/// Load a single serde-deserializable model from a JSON file.
80///
81/// Returns `None` if the file doesn't exist or is malformed.
82fn load_model<T: serde::de::DeserializeOwned>(dir: &Path, filename: &str) -> Option<T> {
83    let path = dir.join(filename);
84    if !path.exists() {
85        return None;
86    }
87    match std::fs::read_to_string(&path) {
88        Ok(data) => match serde_json::from_str::<T>(&data) {
89            Ok(model) => {
90                debug!(path = %path.display(), "loaded EML model");
91                Some(model)
92            }
93            Err(e) => {
94                warn!(path = %path.display(), error = %e, "failed to deserialize EML model, using defaults");
95                None
96            }
97        },
98        Err(e) => {
99            warn!(path = %path.display(), error = %e, "failed to read EML model file");
100            None
101        }
102    }
103}
104
105// ---------------------------------------------------------------------------
106// Public API
107// ---------------------------------------------------------------------------
108
109/// Save all kernel EML models to `dir`.
110///
111/// Creates the directory if it doesn't exist.
112pub fn save_all(dir: &Path, models: &KernelEmlModels) -> std::io::Result<()> {
113    std::fs::create_dir_all(dir)?;
114
115    save_model(dir, "coherence.json", &models.coherence)?;
116    save_model(dir, "governance_scorer.json", &models.governance_scorer)?;
117    save_model(dir, "restart_strategy.json", &models.restart_strategy)?;
118    save_model(dir, "health_threshold.json", &models.health_threshold)?;
119    save_model(dir, "dead_letter.json", &models.dead_letter)?;
120    save_model(dir, "gossip_timing.json", &models.gossip_timing)?;
121    save_model(dir, "complexity.json", &models.complexity)?;
122
123    info!(dir = %dir.display(), "saved all kernel EML models");
124    Ok(())
125}
126
127/// Load all kernel EML models from `dir`.
128///
129/// Falls back to defaults for any model that can't be loaded.
130pub fn load_all(dir: &Path) -> KernelEmlModels {
131    if !dir.exists() {
132        info!(dir = %dir.display(), "EML model directory does not exist, using defaults");
133        return KernelEmlModels::default();
134    }
135
136    let models = KernelEmlModels {
137        coherence: load_model(dir, "coherence.json").unwrap_or_default(),
138        governance_scorer: load_model(dir, "governance_scorer.json").unwrap_or_default(),
139        restart_strategy: load_model(dir, "restart_strategy.json").unwrap_or_default(),
140        health_threshold: load_model(dir, "health_threshold.json").unwrap_or_default(),
141        dead_letter: load_model(dir, "dead_letter.json").unwrap_or_default(),
142        gossip_timing: load_model(dir, "gossip_timing.json").unwrap_or_default(),
143        complexity: load_model(dir, "complexity.json").unwrap_or_default(),
144    };
145
146    info!(dir = %dir.display(), "loaded all kernel EML models");
147    models
148}
149
150// ---------------------------------------------------------------------------
151// Tests
152// ---------------------------------------------------------------------------
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn save_and_load_roundtrip() {
160        let dir = std::env::temp_dir().join("eml_persistence_test");
161        let _ = std::fs::remove_dir_all(&dir);
162
163        let models = KernelEmlModels::default();
164        save_all(&dir, &models).expect("save should succeed");
165
166        let loaded = load_all(&dir);
167
168        // Verify all models loaded with correct trained status
169        assert!(!loaded.coherence.is_trained());
170        assert!(!loaded.governance_scorer.is_trained());
171        assert!(!loaded.restart_strategy.is_trained());
172        assert!(!loaded.health_threshold.is_trained());
173        assert!(!loaded.dead_letter.is_trained());
174        assert!(!loaded.gossip_timing.is_trained());
175        assert!(!loaded.complexity.is_trained());
176
177        let _ = std::fs::remove_dir_all(&dir);
178    }
179
180    #[test]
181    fn load_missing_dir_returns_defaults() {
182        let dir = std::env::temp_dir().join("eml_persistence_missing");
183        let _ = std::fs::remove_dir_all(&dir);
184
185        let models = load_all(&dir);
186        assert!(!models.coherence.is_trained());
187    }
188
189    #[test]
190    fn load_corrupt_file_returns_default() {
191        let dir = std::env::temp_dir().join("eml_persistence_corrupt");
192        let _ = std::fs::remove_dir_all(&dir);
193        std::fs::create_dir_all(&dir).unwrap();
194        std::fs::write(dir.join("coherence.json"), "not valid json").unwrap();
195
196        let models = load_all(&dir);
197        assert!(!models.coherence.is_trained());
198
199        let _ = std::fs::remove_dir_all(&dir);
200    }
201}