1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum MemoryType {
13 Decision,
15 Pattern,
17 Preference,
19 Style,
21 Habit,
23 Insight,
25 Context,
27}
28
29impl std::fmt::Display for MemoryType {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Self::Decision => write!(f, "decision"),
33 Self::Pattern => write!(f, "pattern"),
34 Self::Preference => write!(f, "preference"),
35 Self::Style => write!(f, "style"),
36 Self::Habit => write!(f, "habit"),
37 Self::Insight => write!(f, "insight"),
38 Self::Context => write!(f, "context"),
39 }
40 }
41}
42
43impl std::str::FromStr for MemoryType {
44 type Err = CodememError;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 match s.to_lowercase().as_str() {
48 "decision" => Ok(Self::Decision),
49 "pattern" => Ok(Self::Pattern),
50 "preference" => Ok(Self::Preference),
51 "style" => Ok(Self::Style),
52 "habit" => Ok(Self::Habit),
53 "insight" => Ok(Self::Insight),
54 "context" => Ok(Self::Context),
55 _ => Err(CodememError::InvalidMemoryType(s.to_string())),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
65pub enum RelationshipType {
66 RelatesTo,
68 LeadsTo,
69 PartOf,
70 Reinforces,
72 Contradicts,
73 EvolvedInto,
74 DerivedFrom,
75 InvalidatedBy,
76 DependsOn,
78 Imports,
79 Extends,
80 Calls,
81 Contains,
82 Supersedes,
83 Blocks,
84 Implements,
87 Inherits,
89 SimilarTo,
92 PrecededBy,
94 Exemplifies,
96 Explains,
98 SharesTheme,
100 Summarizes,
102}
103
104impl std::fmt::Display for RelationshipType {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 Self::RelatesTo => write!(f, "RELATES_TO"),
108 Self::LeadsTo => write!(f, "LEADS_TO"),
109 Self::PartOf => write!(f, "PART_OF"),
110 Self::Reinforces => write!(f, "REINFORCES"),
111 Self::Contradicts => write!(f, "CONTRADICTS"),
112 Self::EvolvedInto => write!(f, "EVOLVED_INTO"),
113 Self::DerivedFrom => write!(f, "DERIVED_FROM"),
114 Self::InvalidatedBy => write!(f, "INVALIDATED_BY"),
115 Self::DependsOn => write!(f, "DEPENDS_ON"),
116 Self::Imports => write!(f, "IMPORTS"),
117 Self::Extends => write!(f, "EXTENDS"),
118 Self::Calls => write!(f, "CALLS"),
119 Self::Contains => write!(f, "CONTAINS"),
120 Self::Supersedes => write!(f, "SUPERSEDES"),
121 Self::Blocks => write!(f, "BLOCKS"),
122 Self::Implements => write!(f, "IMPLEMENTS"),
123 Self::Inherits => write!(f, "INHERITS"),
124 Self::SimilarTo => write!(f, "SIMILAR_TO"),
125 Self::PrecededBy => write!(f, "PRECEDED_BY"),
126 Self::Exemplifies => write!(f, "EXEMPLIFIES"),
127 Self::Explains => write!(f, "EXPLAINS"),
128 Self::SharesTheme => write!(f, "SHARES_THEME"),
129 Self::Summarizes => write!(f, "SUMMARIZES"),
130 }
131 }
132}
133
134impl std::str::FromStr for RelationshipType {
135 type Err = CodememError;
136
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 match s.to_uppercase().as_str() {
139 "RELATES_TO" => Ok(Self::RelatesTo),
140 "LEADS_TO" => Ok(Self::LeadsTo),
141 "PART_OF" => Ok(Self::PartOf),
142 "REINFORCES" => Ok(Self::Reinforces),
143 "CONTRADICTS" => Ok(Self::Contradicts),
144 "EVOLVED_INTO" => Ok(Self::EvolvedInto),
145 "DERIVED_FROM" => Ok(Self::DerivedFrom),
146 "INVALIDATED_BY" => Ok(Self::InvalidatedBy),
147 "DEPENDS_ON" => Ok(Self::DependsOn),
148 "IMPORTS" => Ok(Self::Imports),
149 "EXTENDS" => Ok(Self::Extends),
150 "CALLS" => Ok(Self::Calls),
151 "CONTAINS" => Ok(Self::Contains),
152 "SUPERSEDES" => Ok(Self::Supersedes),
153 "BLOCKS" => Ok(Self::Blocks),
154 "IMPLEMENTS" => Ok(Self::Implements),
155 "INHERITS" => Ok(Self::Inherits),
156 "SIMILAR_TO" => Ok(Self::SimilarTo),
157 "PRECEDED_BY" => Ok(Self::PrecededBy),
158 "EXEMPLIFIES" => Ok(Self::Exemplifies),
159 "EXPLAINS" => Ok(Self::Explains),
160 "SHARES_THEME" => Ok(Self::SharesTheme),
161 "SUMMARIZES" => Ok(Self::Summarizes),
162 _ => Err(CodememError::InvalidRelationshipType(s.to_string())),
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum NodeKind {
173 File,
174 Package,
175 Function,
176 Class,
177 Module,
178 Memory,
179 Method,
181 Interface,
183 Type,
185 Constant,
187 Endpoint,
189 Test,
191}
192
193impl std::fmt::Display for NodeKind {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 match self {
196 Self::File => write!(f, "file"),
197 Self::Package => write!(f, "package"),
198 Self::Function => write!(f, "function"),
199 Self::Class => write!(f, "class"),
200 Self::Module => write!(f, "module"),
201 Self::Memory => write!(f, "memory"),
202 Self::Method => write!(f, "method"),
203 Self::Interface => write!(f, "interface"),
204 Self::Type => write!(f, "type"),
205 Self::Constant => write!(f, "constant"),
206 Self::Endpoint => write!(f, "endpoint"),
207 Self::Test => write!(f, "test"),
208 }
209 }
210}
211
212impl std::str::FromStr for NodeKind {
213 type Err = CodememError;
214
215 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 match s.to_lowercase().as_str() {
217 "file" => Ok(Self::File),
218 "package" => Ok(Self::Package),
219 "function" => Ok(Self::Function),
220 "class" => Ok(Self::Class),
221 "module" => Ok(Self::Module),
222 "memory" => Ok(Self::Memory),
223 "method" => Ok(Self::Method),
224 "interface" => Ok(Self::Interface),
225 "type" => Ok(Self::Type),
226 "constant" => Ok(Self::Constant),
227 "endpoint" => Ok(Self::Endpoint),
228 "test" => Ok(Self::Test),
229 _ => Err(CodememError::InvalidNodeKind(s.to_string())),
230 }
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct MemoryNode {
239 pub id: String,
240 pub content: String,
241 pub memory_type: MemoryType,
242 pub importance: f64,
243 pub confidence: f64,
244 pub access_count: u32,
245 pub content_hash: String,
246 pub tags: Vec<String>,
247 pub metadata: HashMap<String, serde_json::Value>,
248 pub namespace: Option<String>,
249 pub created_at: DateTime<Utc>,
250 pub updated_at: DateTime<Utc>,
251 pub last_accessed_at: DateTime<Utc>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Edge {
257 pub id: String,
258 pub src: String,
259 pub dst: String,
260 pub relationship: RelationshipType,
261 pub weight: f64,
262 pub properties: HashMap<String, serde_json::Value>,
263 pub created_at: DateTime<Utc>,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct GraphNode {
269 pub id: String,
270 pub kind: NodeKind,
271 pub label: String,
272 pub payload: HashMap<String, serde_json::Value>,
273 pub centrality: f64,
274 pub memory_id: Option<String>,
275 pub namespace: Option<String>,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct SearchResult {
281 pub memory: MemoryNode,
282 pub score: f64,
283 pub score_breakdown: ScoreBreakdown,
284}
285
286#[derive(Debug, Clone, Default, Serialize, Deserialize)]
288pub struct ScoreBreakdown {
289 pub vector_similarity: f64,
291 pub graph_strength: f64,
293 pub token_overlap: f64,
295 pub temporal: f64,
297 pub tag_matching: f64,
299 pub importance: f64,
301 pub confidence: f64,
303 pub recency: f64,
305}
306
307impl ScoreBreakdown {
308 pub fn total(&self) -> f64 {
310 self.vector_similarity * 0.25
311 + self.graph_strength * 0.25
312 + self.token_overlap * 0.15
313 + self.temporal * 0.10
314 + self.tag_matching * 0.10
315 + self.importance * 0.05
316 + self.confidence * 0.05
317 + self.recency * 0.05
318 }
319
320 pub fn total_with_weights(&self, weights: &ScoringWeights) -> f64 {
322 self.vector_similarity * weights.vector_similarity
323 + self.graph_strength * weights.graph_strength
324 + self.token_overlap * weights.token_overlap
325 + self.temporal * weights.temporal
326 + self.tag_matching * weights.tag_matching
327 + self.importance * weights.importance
328 + self.confidence * weights.confidence
329 + self.recency * weights.recency
330 }
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct ScoringWeights {
339 pub vector_similarity: f64,
340 pub graph_strength: f64,
341 pub token_overlap: f64,
342 pub temporal: f64,
343 pub tag_matching: f64,
344 pub importance: f64,
345 pub confidence: f64,
346 pub recency: f64,
347}
348
349impl Default for ScoringWeights {
350 fn default() -> Self {
351 Self {
352 vector_similarity: 0.25,
353 graph_strength: 0.25,
354 token_overlap: 0.15,
355 temporal: 0.10,
356 tag_matching: 0.10,
357 importance: 0.05,
358 confidence: 0.05,
359 recency: 0.05,
360 }
361 }
362}
363
364impl ScoringWeights {
365 pub fn normalized(&self) -> Self {
367 let sum = self.vector_similarity
368 + self.graph_strength
369 + self.token_overlap
370 + self.temporal
371 + self.tag_matching
372 + self.importance
373 + self.confidence
374 + self.recency;
375 if sum == 0.0 {
376 return Self::default();
377 }
378 Self {
379 vector_similarity: self.vector_similarity / sum,
380 graph_strength: self.graph_strength / sum,
381 token_overlap: self.token_overlap / sum,
382 temporal: self.temporal / sum,
383 tag_matching: self.tag_matching / sum,
384 importance: self.importance / sum,
385 confidence: self.confidence / sum,
386 recency: self.recency / sum,
387 }
388 }
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct VectorConfig {
396 pub dimensions: usize,
397 pub metric: DistanceMetric,
398 pub m: usize,
399 pub ef_construction: usize,
400 pub ef_search: usize,
401}
402
403impl Default for VectorConfig {
404 fn default() -> Self {
405 Self {
406 dimensions: 768,
407 metric: DistanceMetric::Cosine,
408 m: 16,
409 ef_construction: 200,
410 ef_search: 100,
411 }
412 }
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(rename_all = "lowercase")]
418pub enum DistanceMetric {
419 Cosine,
420 L2,
421 InnerProduct,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct GraphConfig {
427 pub max_expansion_hops: usize,
428 pub bridge_similarity_threshold: f64,
429}
430
431impl Default for GraphConfig {
432 fn default() -> Self {
433 Self {
434 max_expansion_hops: 2,
435 bridge_similarity_threshold: 0.8,
436 }
437 }
438}
439
440pub trait VectorBackend: Send + Sync {
444 fn insert(&mut self, id: &str, embedding: &[f32]) -> Result<(), CodememError>;
446
447 fn insert_batch(&mut self, items: &[(String, Vec<f32>)]) -> Result<(), CodememError>;
449
450 fn search(&self, query: &[f32], k: usize) -> Result<Vec<(String, f32)>, CodememError>;
452
453 fn remove(&mut self, id: &str) -> Result<bool, CodememError>;
455
456 fn save(&self, path: &std::path::Path) -> Result<(), CodememError>;
458
459 fn load(&mut self, path: &std::path::Path) -> Result<(), CodememError>;
461
462 fn stats(&self) -> VectorStats;
464}
465
466#[derive(Debug, Clone, Default, Serialize, Deserialize)]
468pub struct VectorStats {
469 pub count: usize,
470 pub dimensions: usize,
471 pub metric: String,
472 pub memory_bytes: usize,
473}
474
475pub trait GraphBackend: Send + Sync {
477 fn add_node(&mut self, node: GraphNode) -> Result<(), CodememError>;
479
480 fn get_node(&self, id: &str) -> Result<Option<GraphNode>, CodememError>;
482
483 fn remove_node(&mut self, id: &str) -> Result<bool, CodememError>;
485
486 fn add_edge(&mut self, edge: Edge) -> Result<(), CodememError>;
488
489 fn get_edges(&self, node_id: &str) -> Result<Vec<Edge>, CodememError>;
491
492 fn remove_edge(&mut self, id: &str) -> Result<bool, CodememError>;
494
495 fn bfs(&self, start_id: &str, max_depth: usize) -> Result<Vec<GraphNode>, CodememError>;
497
498 fn dfs(&self, start_id: &str, max_depth: usize) -> Result<Vec<GraphNode>, CodememError>;
500
501 fn shortest_path(&self, from: &str, to: &str) -> Result<Vec<String>, CodememError>;
503
504 fn stats(&self) -> GraphStats;
506}
507
508#[derive(Debug, Clone, Default, Serialize, Deserialize)]
510pub struct GraphStats {
511 pub node_count: usize,
512 pub edge_count: usize,
513 pub node_kind_counts: HashMap<String, usize>,
514 pub relationship_type_counts: HashMap<String, usize>,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct DetectedPattern {
522 pub pattern_type: PatternType,
524 pub description: String,
526 pub frequency: usize,
528 pub related_memories: Vec<String>,
530 pub confidence: f64,
532}
533
534#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(rename_all = "snake_case")]
537pub enum PatternType {
538 RepeatedSearch,
540 FileHotspot,
542 ExplorationPath,
544 DecisionChain,
546 ToolPreference,
548}
549
550impl std::fmt::Display for PatternType {
551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 match self {
553 Self::RepeatedSearch => write!(f, "repeated_search"),
554 Self::FileHotspot => write!(f, "file_hotspot"),
555 Self::ExplorationPath => write!(f, "exploration_path"),
556 Self::DecisionChain => write!(f, "decision_chain"),
557 Self::ToolPreference => write!(f, "tool_preference"),
558 }
559 }
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
566pub struct Session {
567 pub id: String,
568 pub namespace: Option<String>,
569 pub started_at: DateTime<Utc>,
570 pub ended_at: Option<DateTime<Utc>>,
571 pub memory_count: u32,
572 pub summary: Option<String>,
573}
574
575#[derive(Debug, thiserror::Error)]
579pub enum CodememError {
580 #[error("Storage error: {0}")]
581 Storage(String),
582
583 #[error("Vector error: {0}")]
584 Vector(String),
585
586 #[error("Graph error: {0}")]
587 Graph(String),
588
589 #[error("Embedding error: {0}")]
590 Embedding(String),
591
592 #[error("MCP error: {0}")]
593 Mcp(String),
594
595 #[error("Hook error: {0}")]
596 Hook(String),
597
598 #[error("Invalid memory type: {0}")]
599 InvalidMemoryType(String),
600
601 #[error("Invalid relationship type: {0}")]
602 InvalidRelationshipType(String),
603
604 #[error("Invalid node kind: {0}")]
605 InvalidNodeKind(String),
606
607 #[error("Not found: {0}")]
608 NotFound(String),
609
610 #[error("Duplicate content (hash: {0})")]
611 Duplicate(String),
612
613 #[error("IO error: {0}")]
614 Io(#[from] std::io::Error),
615
616 #[error("JSON error: {0}")]
617 Json(#[from] serde_json::Error),
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn memory_type_roundtrip() {
626 for mt in [
627 MemoryType::Decision,
628 MemoryType::Pattern,
629 MemoryType::Preference,
630 MemoryType::Style,
631 MemoryType::Habit,
632 MemoryType::Insight,
633 MemoryType::Context,
634 ] {
635 let s = mt.to_string();
636 let parsed: MemoryType = s.parse().unwrap();
637 assert_eq!(mt, parsed);
638 }
639 }
640
641 #[test]
642 fn relationship_type_roundtrip() {
643 for rt in [
644 RelationshipType::RelatesTo,
645 RelationshipType::LeadsTo,
646 RelationshipType::PartOf,
647 RelationshipType::Reinforces,
648 RelationshipType::Contradicts,
649 RelationshipType::EvolvedInto,
650 RelationshipType::DerivedFrom,
651 RelationshipType::InvalidatedBy,
652 RelationshipType::DependsOn,
653 RelationshipType::Imports,
654 RelationshipType::Extends,
655 RelationshipType::Calls,
656 RelationshipType::Contains,
657 RelationshipType::Supersedes,
658 RelationshipType::Blocks,
659 RelationshipType::Implements,
660 RelationshipType::Inherits,
661 RelationshipType::SimilarTo,
662 RelationshipType::PrecededBy,
663 RelationshipType::Exemplifies,
664 RelationshipType::Explains,
665 RelationshipType::SharesTheme,
666 RelationshipType::Summarizes,
667 ] {
668 let s = rt.to_string();
669 let parsed: RelationshipType = s.parse().unwrap();
670 assert_eq!(rt, parsed);
671 }
672 }
673
674 #[test]
675 fn node_kind_roundtrip() {
676 for nk in [
677 NodeKind::File,
678 NodeKind::Package,
679 NodeKind::Function,
680 NodeKind::Class,
681 NodeKind::Module,
682 NodeKind::Memory,
683 NodeKind::Method,
684 NodeKind::Interface,
685 NodeKind::Type,
686 NodeKind::Constant,
687 NodeKind::Endpoint,
688 NodeKind::Test,
689 ] {
690 let s = nk.to_string();
691 let parsed: NodeKind = s.parse().unwrap();
692 assert_eq!(nk, parsed);
693 }
694 }
695
696 #[test]
697 fn score_breakdown_weights_sum_to_one() {
698 let breakdown = ScoreBreakdown {
699 vector_similarity: 1.0,
700 graph_strength: 1.0,
701 token_overlap: 1.0,
702 temporal: 1.0,
703 tag_matching: 1.0,
704 importance: 1.0,
705 confidence: 1.0,
706 recency: 1.0,
707 };
708 let total = breakdown.total();
709 assert!((total - 1.0).abs() < f64::EPSILON);
710 }
711
712 #[test]
713 fn default_vector_config() {
714 let config = VectorConfig::default();
715 assert_eq!(config.dimensions, 768);
716 assert_eq!(config.m, 16);
717 assert_eq!(config.ef_construction, 200);
718 assert_eq!(config.ef_search, 100);
719 }
720
721 #[test]
722 fn scoring_weights_default_sum_to_one() {
723 let weights = ScoringWeights::default();
724 let sum = weights.vector_similarity
725 + weights.graph_strength
726 + weights.token_overlap
727 + weights.temporal
728 + weights.tag_matching
729 + weights.importance
730 + weights.confidence
731 + weights.recency;
732 assert!((sum - 1.0).abs() < f64::EPSILON);
733 }
734
735 #[test]
736 fn scoring_weights_normalized() {
737 let weights = ScoringWeights {
738 vector_similarity: 2.0,
739 graph_strength: 2.0,
740 token_overlap: 2.0,
741 temporal: 2.0,
742 tag_matching: 2.0,
743 importance: 2.0,
744 confidence: 2.0,
745 recency: 2.0,
746 };
747 let norm = weights.normalized();
748 let sum = norm.vector_similarity
749 + norm.graph_strength
750 + norm.token_overlap
751 + norm.temporal
752 + norm.tag_matching
753 + norm.importance
754 + norm.confidence
755 + norm.recency;
756 assert!((sum - 1.0).abs() < f64::EPSILON);
757 assert!((norm.vector_similarity - 0.125).abs() < f64::EPSILON);
759 }
760
761 #[test]
762 fn scoring_weights_normalized_zero_returns_default() {
763 let weights = ScoringWeights {
764 vector_similarity: 0.0,
765 graph_strength: 0.0,
766 token_overlap: 0.0,
767 temporal: 0.0,
768 tag_matching: 0.0,
769 importance: 0.0,
770 confidence: 0.0,
771 recency: 0.0,
772 };
773 let norm = weights.normalized();
774 let default = ScoringWeights::default();
775 assert!((norm.vector_similarity - default.vector_similarity).abs() < f64::EPSILON);
776 assert!((norm.graph_strength - default.graph_strength).abs() < f64::EPSILON);
777 }
778
779 #[test]
780 fn total_with_weights_matches_total_for_defaults() {
781 let breakdown = ScoreBreakdown {
782 vector_similarity: 0.8,
783 graph_strength: 0.6,
784 token_overlap: 0.5,
785 temporal: 0.9,
786 tag_matching: 0.3,
787 importance: 0.7,
788 confidence: 0.95,
789 recency: 0.4,
790 };
791 let default_weights = ScoringWeights::default();
792 let total = breakdown.total();
793 let total_with = breakdown.total_with_weights(&default_weights);
794 assert!((total - total_with).abs() < f64::EPSILON);
795 }
796
797 #[test]
798 fn total_with_weights_custom() {
799 let breakdown = ScoreBreakdown {
800 vector_similarity: 1.0,
801 graph_strength: 0.0,
802 token_overlap: 0.0,
803 temporal: 0.0,
804 tag_matching: 0.0,
805 importance: 0.0,
806 confidence: 0.0,
807 recency: 0.0,
808 };
809 let weights = ScoringWeights {
811 vector_similarity: 1.0,
812 graph_strength: 0.0,
813 token_overlap: 0.0,
814 temporal: 0.0,
815 tag_matching: 0.0,
816 importance: 0.0,
817 confidence: 0.0,
818 recency: 0.0,
819 };
820 let total = breakdown.total_with_weights(&weights);
821 assert!((total - 1.0).abs() < f64::EPSILON);
822 }
823
824 #[test]
825 fn pattern_type_display() {
826 assert_eq!(PatternType::RepeatedSearch.to_string(), "repeated_search");
827 assert_eq!(PatternType::FileHotspot.to_string(), "file_hotspot");
828 assert_eq!(PatternType::ExplorationPath.to_string(), "exploration_path");
829 assert_eq!(PatternType::DecisionChain.to_string(), "decision_chain");
830 assert_eq!(PatternType::ToolPreference.to_string(), "tool_preference");
831 }
832
833 #[test]
834 fn detected_pattern_serialization() {
835 let pattern = DetectedPattern {
836 pattern_type: PatternType::RepeatedSearch,
837 description: "Search for 'error handling' appears 5 times".to_string(),
838 frequency: 5,
839 related_memories: vec!["mem-1".to_string(), "mem-2".to_string()],
840 confidence: 0.85,
841 };
842 let json = serde_json::to_string(&pattern).unwrap();
843 let parsed: DetectedPattern = serde_json::from_str(&json).unwrap();
844 assert_eq!(parsed.pattern_type, PatternType::RepeatedSearch);
845 assert_eq!(parsed.frequency, 5);
846 assert_eq!(parsed.related_memories.len(), 2);
847 }
848}