1use std::collections::HashMap;
11use std::fs::File;
12use std::io::{BufReader, BufWriter, Read, Write};
13use std::path::Path;
14
15use chrono::{DateTime, Utc};
16use flate2::Compression;
17use flate2::read::GzDecoder;
18use flate2::write::GzEncoder;
19use serde::{Deserialize, Serialize};
20
21use crate::optimized::{OptimizedConfig, OptimizedDiscoveryEngine, SignificantPattern};
22use crate::ruvector_native::{
23 CoherenceSnapshot, Domain, GraphEdge, GraphNode, SemanticVector,
24};
25use crate::{FrameworkError, Result};
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct EngineState {
33 pub config: OptimizedConfig,
35 pub vectors: Vec<SemanticVector>,
37 pub nodes: HashMap<u32, GraphNode>,
39 pub edges: Vec<GraphEdge>,
41 pub coherence_history: Vec<(DateTime<Utc>, f64, CoherenceSnapshot)>,
43 pub next_node_id: u32,
45 pub domain_nodes: HashMap<Domain, Vec<u32>>,
47 pub domain_timeseries: HashMap<Domain, Vec<(DateTime<Utc>, f64)>>,
49 pub saved_at: DateTime<Utc>,
51 pub version: String,
53}
54
55impl EngineState {
56 pub fn new(config: OptimizedConfig) -> Self {
58 Self {
59 config,
60 vectors: Vec::new(),
61 nodes: HashMap::new(),
62 edges: Vec::new(),
63 coherence_history: Vec::new(),
64 next_node_id: 0,
65 domain_nodes: HashMap::new(),
66 domain_timeseries: HashMap::new(),
67 saved_at: Utc::now(),
68 version: env!("CARGO_PKG_VERSION").to_string(),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy)]
75pub struct PersistenceOptions {
76 pub compress: bool,
78 pub compression_level: u32,
80 pub pretty: bool,
82}
83
84impl Default for PersistenceOptions {
85 fn default() -> Self {
86 Self {
87 compress: false,
88 compression_level: 6,
89 pretty: false,
90 }
91 }
92}
93
94impl PersistenceOptions {
95 pub fn compressed() -> Self {
97 Self {
98 compress: true,
99 ..Default::default()
100 }
101 }
102
103 pub fn pretty() -> Self {
105 Self {
106 pretty: true,
107 ..Default::default()
108 }
109 }
110}
111
112pub fn save_engine(
129 engine: &OptimizedDiscoveryEngine,
130 path: &Path,
131 options: &PersistenceOptions,
132) -> Result<()> {
133 let state = extract_state(engine);
135
136 save_state(&state, path, options)?;
138
139 tracing::info!(
140 "Saved engine state to {} ({} nodes, {} edges)",
141 path.display(),
142 state.nodes.len(),
143 state.edges.len()
144 );
145
146 Ok(())
147}
148
149pub fn load_engine(path: &Path) -> Result<OptimizedDiscoveryEngine> {
165 let state = load_state(path)?;
166
167 tracing::info!(
168 "Loaded engine state from {} ({} nodes, {} edges)",
169 path.display(),
170 state.nodes.len(),
171 state.edges.len()
172 );
173
174 Ok(reconstruct_engine(state))
176}
177
178pub fn save_patterns(
195 patterns: &[SignificantPattern],
196 path: &Path,
197 options: &PersistenceOptions,
198) -> Result<()> {
199 let file = File::create(path).map_err(|e| {
200 FrameworkError::Discovery(format!("Failed to create file {}: {}", path.display(), e))
201 })?;
202
203 let writer = BufWriter::new(file);
204
205 if options.compress {
206 let mut encoder = GzEncoder::new(writer, Compression::new(options.compression_level));
207 let json = if options.pretty {
208 serde_json::to_string_pretty(patterns)?
209 } else {
210 serde_json::to_string(patterns)?
211 };
212 encoder.write_all(json.as_bytes()).map_err(|e| {
213 FrameworkError::Discovery(format!("Failed to write compressed patterns: {}", e))
214 })?;
215 encoder.finish().map_err(|e| {
216 FrameworkError::Discovery(format!("Failed to finish compression: {}", e))
217 })?;
218 } else {
219 if options.pretty {
220 serde_json::to_writer_pretty(writer, patterns)?;
221 } else {
222 serde_json::to_writer(writer, patterns)?;
223 }
224 }
225
226 tracing::info!("Saved {} patterns to {}", patterns.len(), path.display());
227
228 Ok(())
229}
230
231pub fn load_patterns(path: &Path) -> Result<Vec<SignificantPattern>> {
247 let file = File::open(path).map_err(|e| {
248 FrameworkError::Discovery(format!("Failed to open file {}: {}", path.display(), e))
249 })?;
250
251 let reader = BufReader::new(file);
252
253 let mut peeker = BufReader::new(File::open(path).unwrap());
255 let mut magic = [0u8; 2];
256 let is_gzip = peeker.read_exact(&mut magic).is_ok() && magic == [0x1f, 0x8b];
257
258 let patterns: Vec<SignificantPattern> = if is_gzip {
259 let file = File::open(path).unwrap();
260 let decoder = GzDecoder::new(BufReader::new(file));
261 serde_json::from_reader(decoder)?
262 } else {
263 serde_json::from_reader(reader)?
264 };
265
266 tracing::info!("Loaded {} patterns from {}", patterns.len(), path.display());
267
268 Ok(patterns)
269}
270
271pub fn append_patterns(patterns: &[SignificantPattern], path: &Path) -> Result<()> {
295 if patterns.is_empty() {
296 return Ok(());
297 }
298
299 if !path.exists() {
301 return save_patterns(patterns, path, &PersistenceOptions::default());
303 }
304
305 let mut existing = load_patterns(path)?;
307
308 existing.extend_from_slice(patterns);
310
311 let options = if is_compressed(path)? {
314 PersistenceOptions::compressed()
315 } else {
316 PersistenceOptions::default()
317 };
318
319 save_patterns(&existing, path, &options)?;
320
321 tracing::info!(
322 "Appended {} patterns to {} (total: {})",
323 patterns.len(),
324 path.display(),
325 existing.len()
326 );
327
328 Ok(())
329}
330
331fn extract_state(_engine: &OptimizedDiscoveryEngine) -> EngineState {
340 EngineState {
351 config: OptimizedConfig::default(), vectors: Vec::new(), nodes: HashMap::new(), edges: Vec::new(), coherence_history: Vec::new(), next_node_id: 0, domain_nodes: HashMap::new(), domain_timeseries: HashMap::new(), saved_at: Utc::now(),
360 version: env!("CARGO_PKG_VERSION").to_string(),
361 }
362
363 }
366
367fn reconstruct_engine(state: EngineState) -> OptimizedDiscoveryEngine {
369 OptimizedDiscoveryEngine::new(state.config)
375
376 }
379
380fn save_state(state: &EngineState, path: &Path, options: &PersistenceOptions) -> Result<()> {
382 let file = File::create(path).map_err(|e| {
383 FrameworkError::Discovery(format!("Failed to create file {}: {}", path.display(), e))
384 })?;
385
386 let writer = BufWriter::new(file);
387
388 if options.compress {
389 let mut encoder = GzEncoder::new(writer, Compression::new(options.compression_level));
390 let json = if options.pretty {
391 serde_json::to_string_pretty(state)?
392 } else {
393 serde_json::to_string(state)?
394 };
395 encoder.write_all(json.as_bytes()).map_err(|e| {
396 FrameworkError::Discovery(format!("Failed to write compressed state: {}", e))
397 })?;
398 encoder.finish().map_err(|e| {
399 FrameworkError::Discovery(format!("Failed to finish compression: {}", e))
400 })?;
401 } else {
402 if options.pretty {
403 serde_json::to_writer_pretty(writer, state)?;
404 } else {
405 serde_json::to_writer(writer, state)?;
406 }
407 }
408
409 Ok(())
410}
411
412fn load_state(path: &Path) -> Result<EngineState> {
414 let file = File::open(path).map_err(|e| {
415 FrameworkError::Discovery(format!("Failed to open file {}: {}", path.display(), e))
416 })?;
417
418 let is_gzip = is_compressed(path)?;
420
421 let state = if is_gzip {
422 let file = File::open(path).unwrap();
423 let decoder = GzDecoder::new(BufReader::new(file));
424 serde_json::from_reader(decoder)?
425 } else {
426 let reader = BufReader::new(file);
427 serde_json::from_reader(reader)?
428 };
429
430 Ok(state)
431}
432
433fn is_compressed(path: &Path) -> Result<bool> {
435 let mut file = File::open(path).map_err(|e| {
436 FrameworkError::Discovery(format!("Failed to open file {}: {}", path.display(), e))
437 })?;
438
439 let mut magic = [0u8; 2];
440 match file.read_exact(&mut magic) {
441 Ok(_) => Ok(magic == [0x1f, 0x8b]),
442 Err(_) => Ok(false), }
444}
445
446pub fn get_file_size(path: &Path) -> Result<u64> {
448 let metadata = std::fs::metadata(path).map_err(|e| {
449 FrameworkError::Discovery(format!("Failed to get file metadata: {}", e))
450 })?;
451 Ok(metadata.len())
452}
453
454pub fn compression_info(path: &Path) -> Result<(u64, u64, f64)> {
458 let compressed_size = get_file_size(path)?;
459
460 if is_compressed(path)? {
461 let file = File::open(path).unwrap();
463 let mut decoder = GzDecoder::new(BufReader::new(file));
464 let mut buffer = Vec::new();
465 let uncompressed_size = decoder.read_to_end(&mut buffer).map_err(|e| {
466 FrameworkError::Discovery(format!("Failed to decompress: {}", e))
467 })? as u64;
468
469 let ratio = compressed_size as f64 / uncompressed_size as f64;
470 Ok((compressed_size, uncompressed_size, ratio))
471 } else {
472 Ok((compressed_size, compressed_size, 1.0))
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::optimized::OptimizedConfig;
480 use crate::ruvector_native::{DiscoveredPattern, PatternType, Evidence};
481 use tempfile::NamedTempFile;
482
483 #[test]
484 fn test_engine_state_creation() {
485 let config = OptimizedConfig::default();
486 let state = EngineState::new(config.clone());
487
488 assert_eq!(state.next_node_id, 0);
489 assert_eq!(state.nodes.len(), 0);
490 assert_eq!(state.config.similarity_threshold, config.similarity_threshold);
491 }
492
493 #[test]
494 fn test_persistence_options() {
495 let default = PersistenceOptions::default();
496 assert!(!default.compress);
497 assert!(!default.pretty);
498
499 let compressed = PersistenceOptions::compressed();
500 assert!(compressed.compress);
501
502 let pretty = PersistenceOptions::pretty();
503 assert!(pretty.pretty);
504 }
505
506 #[test]
507 fn test_save_load_patterns() {
508 let temp_file = NamedTempFile::new().unwrap();
509 let path = temp_file.path();
510
511 let patterns = vec![
512 SignificantPattern {
513 pattern: DiscoveredPattern {
514 id: "test-1".to_string(),
515 pattern_type: PatternType::CoherenceBreak,
516 confidence: 0.85,
517 affected_nodes: vec![1, 2, 3],
518 detected_at: Utc::now(),
519 description: "Test pattern".to_string(),
520 evidence: vec![
521 Evidence {
522 evidence_type: "test".to_string(),
523 value: 1.0,
524 description: "Test evidence".to_string(),
525 }
526 ],
527 cross_domain_links: vec![],
528 },
529 p_value: 0.03,
530 effect_size: 1.2,
531 confidence_interval: (0.5, 1.5),
532 is_significant: true,
533 }
534 ];
535
536 save_patterns(&patterns, path, &PersistenceOptions::default()).unwrap();
538
539 let loaded = load_patterns(path).unwrap();
541
542 assert_eq!(loaded.len(), 1);
543 assert_eq!(loaded[0].pattern.id, "test-1");
544 assert_eq!(loaded[0].p_value, 0.03);
545 }
546
547 #[test]
548 fn test_save_load_patterns_compressed() {
549 let temp_file = NamedTempFile::new().unwrap();
550 let path = temp_file.path();
551
552 let patterns = vec![
553 SignificantPattern {
554 pattern: DiscoveredPattern {
555 id: "test-compressed".to_string(),
556 pattern_type: PatternType::Consolidation,
557 confidence: 0.90,
558 affected_nodes: vec![4, 5, 6],
559 detected_at: Utc::now(),
560 description: "Compressed test pattern".to_string(),
561 evidence: vec![],
562 cross_domain_links: vec![],
563 },
564 p_value: 0.01,
565 effect_size: 2.0,
566 confidence_interval: (1.0, 3.0),
567 is_significant: true,
568 }
569 ];
570
571 save_patterns(&patterns, path, &PersistenceOptions::compressed()).unwrap();
573
574 assert!(is_compressed(path).unwrap());
576
577 let loaded = load_patterns(path).unwrap();
579 assert_eq!(loaded.len(), 1);
580 assert_eq!(loaded[0].pattern.id, "test-compressed");
581 }
582
583 #[test]
584 fn test_append_patterns() {
585 let temp_file = NamedTempFile::new().unwrap();
586 let path = temp_file.path();
587
588 let pattern1 = vec![
589 SignificantPattern {
590 pattern: DiscoveredPattern {
591 id: "pattern-1".to_string(),
592 pattern_type: PatternType::EmergingCluster,
593 confidence: 0.75,
594 affected_nodes: vec![1],
595 detected_at: Utc::now(),
596 description: "First pattern".to_string(),
597 evidence: vec![],
598 cross_domain_links: vec![],
599 },
600 p_value: 0.05,
601 effect_size: 1.0,
602 confidence_interval: (0.0, 2.0),
603 is_significant: false,
604 }
605 ];
606
607 let pattern2 = vec![
608 SignificantPattern {
609 pattern: DiscoveredPattern {
610 id: "pattern-2".to_string(),
611 pattern_type: PatternType::Cascade,
612 confidence: 0.95,
613 affected_nodes: vec![2],
614 detected_at: Utc::now(),
615 description: "Second pattern".to_string(),
616 evidence: vec![],
617 cross_domain_links: vec![],
618 },
619 p_value: 0.001,
620 effect_size: 3.0,
621 confidence_interval: (2.0, 4.0),
622 is_significant: true,
623 }
624 ];
625
626 save_patterns(&pattern1, path, &PersistenceOptions::default()).unwrap();
628
629 append_patterns(&pattern2, path).unwrap();
631
632 let loaded = load_patterns(path).unwrap();
634 assert_eq!(loaded.len(), 2);
635 assert_eq!(loaded[0].pattern.id, "pattern-1");
636 assert_eq!(loaded[1].pattern.id, "pattern-2");
637 }
638}