Skip to main content

codemem_core/
lib.rs

1//! codemem-core: Shared types, traits, and errors for the Codemem memory engine.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7// ── Memory Types ────────────────────────────────────────────────────────────
8
9/// The 7 memory types inspired by AutoMem research.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum MemoryType {
13    /// Architectural and design decisions made during development.
14    Decision,
15    /// Recurring code patterns observed across files.
16    Pattern,
17    /// Team/project preferences (e.g., "prefers explicit error types").
18    Preference,
19    /// Coding style norms (e.g., "early returns, max 20 lines").
20    Style,
21    /// Workflow habits (e.g., "tests written before implementation").
22    Habit,
23    /// Cross-domain insights discovered during consolidation.
24    Insight,
25    /// File contents and structural context from code exploration.
26    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// ── Relationship Types ──────────────────────────────────────────────────────
61
62/// 15 relationship types for the knowledge graph.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
65pub enum RelationshipType {
66    // General
67    RelatesTo,
68    LeadsTo,
69    PartOf,
70    // Knowledge
71    Reinforces,
72    Contradicts,
73    EvolvedInto,
74    DerivedFrom,
75    InvalidatedBy,
76    // Code-specific
77    DependsOn,
78    Imports,
79    Extends,
80    Calls,
81    Contains,
82    Supersedes,
83    Blocks,
84    // Structural (auto-created by indexing)
85    /// Implements interface/trait.
86    Implements,
87    /// Class inheritance.
88    Inherits,
89    // Semantic (auto-created by enrichment)
90    /// Semantic similarity > threshold.
91    SimilarTo,
92    /// Temporal adjacency.
93    PrecededBy,
94    /// Memory exemplifies a pattern.
95    Exemplifies,
96    /// Insight explains a pattern.
97    Explains,
98    /// High similarity across types (consolidation).
99    SharesTheme,
100    /// Meta-memory summarizes a cluster.
101    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// ── Graph Node Types ────────────────────────────────────────────────────────
168
169/// Node types in the knowledge graph.
170#[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    /// Class method (distinct from standalone function).
180    Method,
181    /// TypeScript interface, Go interface, Rust trait.
182    Interface,
183    /// Type alias, typedef.
184    Type,
185    /// Const, static, enum variant.
186    Constant,
187    /// REST/gRPC endpoint definition.
188    Endpoint,
189    /// Test function.
190    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// ── Core Data Structures ────────────────────────────────────────────────────
235
236/// A memory node stored in the database.
237#[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/// A graph edge connecting two nodes.
255#[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/// A graph node in the knowledge graph.
267#[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/// A search result with hybrid scoring.
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct SearchResult {
281    pub memory: MemoryNode,
282    pub score: f64,
283    pub score_breakdown: ScoreBreakdown,
284}
285
286/// Breakdown of the 9-component hybrid scoring.
287#[derive(Debug, Clone, Default, Serialize, Deserialize)]
288pub struct ScoreBreakdown {
289    /// Vector cosine similarity (25%)
290    pub vector_similarity: f64,
291    /// Graph relationship strength (25%)
292    pub graph_strength: f64,
293    /// Content token overlap (15%)
294    pub token_overlap: f64,
295    /// Temporal alignment (10%)
296    pub temporal: f64,
297    /// Tag matching (10%)
298    pub tag_matching: f64,
299    /// Importance score (5%)
300    pub importance: f64,
301    /// Memory confidence (5%)
302    pub confidence: f64,
303    /// Recency boost (5%)
304    pub recency: f64,
305}
306
307impl ScoreBreakdown {
308    /// Compute the weighted total score using default weights.
309    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    /// Compute the weighted total score using configurable weights.
321    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// ── Scoring Weights ──────────────────────────────────────────────────────────
334
335/// Configurable weights for the 9-component hybrid scoring system.
336/// All weights should sum to 1.0.
337#[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    /// Normalize weights so they sum to 1.0.
366    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// ── Configuration ───────────────────────────────────────────────────────────
392
393/// Configuration for the HNSW vector index.
394#[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/// Distance metric for vector search.
416#[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/// Configuration for the graph engine.
425#[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
440// ── Traits ──────────────────────────────────────────────────────────────────
441
442/// Vector backend trait for HNSW index operations.
443pub trait VectorBackend: Send + Sync {
444    /// Insert a vector with associated ID.
445    fn insert(&mut self, id: &str, embedding: &[f32]) -> Result<(), CodememError>;
446
447    /// Batch insert vectors.
448    fn insert_batch(&mut self, items: &[(String, Vec<f32>)]) -> Result<(), CodememError>;
449
450    /// Search for k nearest neighbors. Returns (id, distance) pairs.
451    fn search(&self, query: &[f32], k: usize) -> Result<Vec<(String, f32)>, CodememError>;
452
453    /// Remove a vector by ID.
454    fn remove(&mut self, id: &str) -> Result<bool, CodememError>;
455
456    /// Save the index to disk.
457    fn save(&self, path: &std::path::Path) -> Result<(), CodememError>;
458
459    /// Load the index from disk.
460    fn load(&mut self, path: &std::path::Path) -> Result<(), CodememError>;
461
462    /// Get index statistics.
463    fn stats(&self) -> VectorStats;
464}
465
466/// Statistics about the vector index.
467#[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
475/// Graph backend trait for graph operations.
476pub trait GraphBackend: Send + Sync {
477    /// Add a node to the graph.
478    fn add_node(&mut self, node: GraphNode) -> Result<(), CodememError>;
479
480    /// Get a node by ID.
481    fn get_node(&self, id: &str) -> Result<Option<GraphNode>, CodememError>;
482
483    /// Remove a node by ID.
484    fn remove_node(&mut self, id: &str) -> Result<bool, CodememError>;
485
486    /// Add an edge between two nodes.
487    fn add_edge(&mut self, edge: Edge) -> Result<(), CodememError>;
488
489    /// Get edges from a node.
490    fn get_edges(&self, node_id: &str) -> Result<Vec<Edge>, CodememError>;
491
492    /// Remove an edge by ID.
493    fn remove_edge(&mut self, id: &str) -> Result<bool, CodememError>;
494
495    /// BFS traversal from a start node up to max_depth.
496    fn bfs(&self, start_id: &str, max_depth: usize) -> Result<Vec<GraphNode>, CodememError>;
497
498    /// DFS traversal from a start node up to max_depth.
499    fn dfs(&self, start_id: &str, max_depth: usize) -> Result<Vec<GraphNode>, CodememError>;
500
501    /// Shortest path between two nodes.
502    fn shortest_path(&self, from: &str, to: &str) -> Result<Vec<String>, CodememError>;
503
504    /// Get graph statistics.
505    fn stats(&self) -> GraphStats;
506}
507
508/// Statistics about the graph.
509#[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// ── Cross-Session Pattern Detection ─────────────────────────────────────────
518
519/// A pattern detected across sessions by analyzing memory metadata.
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct DetectedPattern {
522    /// Type of pattern detected.
523    pub pattern_type: PatternType,
524    /// Human-readable description of the pattern.
525    pub description: String,
526    /// How many times this pattern was observed.
527    pub frequency: usize,
528    /// IDs of memories related to this pattern.
529    pub related_memories: Vec<String>,
530    /// Confidence in the detection (0.0 to 1.0).
531    pub confidence: f64,
532}
533
534/// Types of cross-session patterns that can be detected.
535#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(rename_all = "snake_case")]
537pub enum PatternType {
538    /// Same search pattern (Grep/Glob) used multiple times across sessions.
539    RepeatedSearch,
540    /// A file that is read or edited frequently across sessions.
541    FileHotspot,
542    /// A sequence of file explorations forming a navigation path.
543    ExplorationPath,
544    /// Multiple edits/writes to the same file over time, forming a decision chain.
545    DecisionChain,
546    /// Disproportionate usage of certain tools over others.
547    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// ── Sessions ────────────────────────────────────────────────────────────
563
564/// A session representing a single interaction period with an AI assistant.
565#[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// ── Errors ──────────────────────────────────────────────────────────────
576
577/// Unified error type for Codemem.
578#[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        // All equal => each should be 0.125
758        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        // Weight only vector_similarity at 1.0, rest 0.0
810        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}