Skip to main content

mentedb_core/
config.rs

1//! Configuration types for MenteDB.
2
3use serde::Deserialize;
4use std::path::Path;
5
6use crate::error::{MenteError, MenteResult};
7
8/// Top-level configuration for a MenteDB instance.
9#[derive(Debug, Clone, Deserialize, Default)]
10pub struct MenteConfig {
11    /// Storage engine configuration.
12    #[serde(default)]
13    pub storage: StorageConfig,
14    /// Index layer configuration.
15    #[serde(default)]
16    pub index: IndexConfig,
17    /// Context assembly configuration.
18    #[serde(default)]
19    pub context: ContextConfig,
20    /// Cognitive engine configuration.
21    #[serde(default)]
22    pub cognitive: CognitiveConfig,
23    /// Memory consolidation configuration.
24    #[serde(default)]
25    pub consolidation: ConsolidationConfig,
26    /// Server configuration.
27    #[serde(default)]
28    pub server: ServerConfig,
29}
30
31/// Storage engine settings.
32#[derive(Debug, Clone, Deserialize)]
33pub struct StorageConfig {
34    /// Directory for data files.
35    pub data_dir: String,
36    /// Number of pages in the buffer pool.
37    #[serde(default = "default_buffer_pool_size")]
38    pub buffer_pool_size: usize,
39    /// Page size in bytes.
40    #[serde(default = "default_page_size")]
41    pub page_size: usize,
42}
43
44fn default_buffer_pool_size() -> usize {
45    1024
46}
47fn default_page_size() -> usize {
48    16384
49}
50
51impl Default for StorageConfig {
52    fn default() -> Self {
53        Self {
54            data_dir: "data".to_string(),
55            buffer_pool_size: default_buffer_pool_size(),
56            page_size: default_page_size(),
57        }
58    }
59}
60
61/// HNSW index settings.
62#[derive(Debug, Clone, Deserialize)]
63pub struct IndexConfig {
64    /// Number of bidirectional links per node.
65    #[serde(default = "default_hnsw_m")]
66    pub hnsw_m: usize,
67    /// Size of the dynamic candidate list during construction.
68    #[serde(default = "default_hnsw_ef_construction")]
69    pub hnsw_ef_construction: usize,
70    /// Size of the dynamic candidate list during search.
71    #[serde(default = "default_hnsw_ef_search")]
72    pub hnsw_ef_search: usize,
73}
74
75fn default_hnsw_m() -> usize {
76    16
77}
78fn default_hnsw_ef_construction() -> usize {
79    200
80}
81fn default_hnsw_ef_search() -> usize {
82    50
83}
84
85impl Default for IndexConfig {
86    fn default() -> Self {
87        Self {
88            hnsw_m: default_hnsw_m(),
89            hnsw_ef_construction: default_hnsw_ef_construction(),
90            hnsw_ef_search: default_hnsw_ef_search(),
91        }
92    }
93}
94
95/// Context assembly settings.
96#[derive(Debug, Clone, Deserialize)]
97pub struct ContextConfig {
98    /// Default token budget for context windows.
99    #[serde(default = "default_token_budget")]
100    pub default_token_budget: usize,
101    /// Multiplier for estimating token counts from word counts.
102    #[serde(default = "default_token_multiplier")]
103    pub token_multiplier: f32,
104    /// Fraction of budget for the system zone.
105    #[serde(default = "default_zone_system_pct")]
106    pub zone_system_pct: f32,
107    /// Fraction of budget for the critical zone.
108    #[serde(default = "default_zone_critical_pct")]
109    pub zone_critical_pct: f32,
110    /// Fraction of budget for the primary zone.
111    #[serde(default = "default_zone_primary_pct")]
112    pub zone_primary_pct: f32,
113    /// Fraction of budget for the supporting zone.
114    #[serde(default = "default_zone_supporting_pct")]
115    pub zone_supporting_pct: f32,
116    /// Fraction of budget for the reference zone.
117    #[serde(default = "default_zone_reference_pct")]
118    pub zone_reference_pct: f32,
119}
120
121fn default_token_budget() -> usize {
122    4096
123}
124fn default_token_multiplier() -> f32 {
125    1.3
126}
127fn default_zone_system_pct() -> f32 {
128    0.10
129}
130fn default_zone_critical_pct() -> f32 {
131    0.25
132}
133fn default_zone_primary_pct() -> f32 {
134    0.35
135}
136fn default_zone_supporting_pct() -> f32 {
137    0.20
138}
139fn default_zone_reference_pct() -> f32 {
140    0.10
141}
142
143impl Default for ContextConfig {
144    fn default() -> Self {
145        Self {
146            default_token_budget: default_token_budget(),
147            token_multiplier: default_token_multiplier(),
148            zone_system_pct: default_zone_system_pct(),
149            zone_critical_pct: default_zone_critical_pct(),
150            zone_primary_pct: default_zone_primary_pct(),
151            zone_supporting_pct: default_zone_supporting_pct(),
152            zone_reference_pct: default_zone_reference_pct(),
153        }
154    }
155}
156
157/// Cognitive engine settings.
158#[derive(Debug, Clone, Deserialize)]
159pub struct CognitiveConfig {
160    /// Similarity threshold above which memories are considered contradictory.
161    #[serde(default = "default_contradiction_threshold")]
162    pub contradiction_threshold: f32,
163    /// Minimum similarity for memories to be considered related.
164    #[serde(default = "default_related_threshold_min")]
165    pub related_threshold_min: f32,
166    /// Maximum similarity for the "related" band (above this is near-duplicate).
167    #[serde(default = "default_related_threshold_max")]
168    pub related_threshold_max: f32,
169    /// Similarity threshold for interference detection.
170    #[serde(default = "default_interference_threshold")]
171    pub interference_threshold: f32,
172    /// Number of entries in the speculative pre-assembly cache.
173    #[serde(default = "default_speculative_cache_size")]
174    pub speculative_cache_size: usize,
175    /// Hit rate threshold for the speculative cache to remain active.
176    #[serde(default = "default_speculative_hit_threshold")]
177    pub speculative_hit_threshold: f32,
178    /// Maximum number of turns to track in a trajectory.
179    #[serde(default = "default_max_trajectory_turns")]
180    pub max_trajectory_turns: usize,
181    /// Maximum number of active pain signal warnings.
182    #[serde(default = "default_max_pain_warnings")]
183    pub max_pain_warnings: usize,
184    /// Maximum number of active phantom memory warnings.
185    #[serde(default = "default_max_phantom_warnings")]
186    pub max_phantom_warnings: usize,
187}
188
189fn default_contradiction_threshold() -> f32 {
190    0.95
191}
192fn default_related_threshold_min() -> f32 {
193    0.6
194}
195fn default_related_threshold_max() -> f32 {
196    0.85
197}
198fn default_interference_threshold() -> f32 {
199    0.8
200}
201fn default_speculative_cache_size() -> usize {
202    10
203}
204fn default_speculative_hit_threshold() -> f32 {
205    0.5
206}
207fn default_max_trajectory_turns() -> usize {
208    100
209}
210fn default_max_pain_warnings() -> usize {
211    5
212}
213fn default_max_phantom_warnings() -> usize {
214    5
215}
216
217impl Default for CognitiveConfig {
218    fn default() -> Self {
219        Self {
220            contradiction_threshold: default_contradiction_threshold(),
221            related_threshold_min: default_related_threshold_min(),
222            related_threshold_max: default_related_threshold_max(),
223            interference_threshold: default_interference_threshold(),
224            speculative_cache_size: default_speculative_cache_size(),
225            speculative_hit_threshold: default_speculative_hit_threshold(),
226            max_trajectory_turns: default_max_trajectory_turns(),
227            max_pain_warnings: default_max_pain_warnings(),
228            max_phantom_warnings: default_max_phantom_warnings(),
229        }
230    }
231}
232
233/// Memory consolidation settings.
234#[derive(Debug, Clone, Deserialize)]
235pub struct ConsolidationConfig {
236    /// Half-life for temporal salience decay, in hours.
237    #[serde(default = "default_decay_half_life_hours")]
238    pub decay_half_life_hours: f64,
239    /// Minimum salience before a memory is eligible for archival.
240    #[serde(default = "default_min_salience")]
241    pub min_salience: f32,
242    /// Minimum age in days before a memory can be archived.
243    #[serde(default = "default_archival_min_age_days")]
244    pub archival_min_age_days: u64,
245    /// Maximum salience for archival eligibility.
246    #[serde(default = "default_archival_max_salience")]
247    pub archival_max_salience: f32,
248}
249
250fn default_decay_half_life_hours() -> f64 {
251    168.0
252}
253fn default_min_salience() -> f32 {
254    0.01
255}
256fn default_archival_min_age_days() -> u64 {
257    30
258}
259fn default_archival_max_salience() -> f32 {
260    0.05
261}
262
263impl Default for ConsolidationConfig {
264    fn default() -> Self {
265        Self {
266            decay_half_life_hours: default_decay_half_life_hours(),
267            min_salience: default_min_salience(),
268            archival_min_age_days: default_archival_min_age_days(),
269            archival_max_salience: default_archival_max_salience(),
270        }
271    }
272}
273
274/// Server settings.
275#[derive(Debug, Clone, Deserialize)]
276pub struct ServerConfig {
277    /// Bind address.
278    #[serde(default = "default_host")]
279    pub host: String,
280    /// Listen port.
281    #[serde(default = "default_port")]
282    pub port: u16,
283}
284
285fn default_host() -> String {
286    "0.0.0.0".to_string()
287}
288fn default_port() -> u16 {
289    6677
290}
291
292impl Default for ServerConfig {
293    fn default() -> Self {
294        Self {
295            host: default_host(),
296            port: default_port(),
297        }
298    }
299}
300
301impl MenteConfig {
302    /// Load configuration from a JSON file.
303    pub fn from_file(path: &Path) -> MenteResult<Self> {
304        let contents = std::fs::read_to_string(path).map_err(MenteError::Io)?;
305        serde_json::from_str(&contents).map_err(|e| MenteError::Serialization(e.to_string()))
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312
313    #[test]
314    fn test_defaults() {
315        let cfg = MenteConfig::default();
316
317        assert_eq!(cfg.storage.data_dir, "data");
318        assert_eq!(cfg.storage.buffer_pool_size, 1024);
319        assert_eq!(cfg.storage.page_size, 16384);
320
321        assert_eq!(cfg.index.hnsw_m, 16);
322        assert_eq!(cfg.index.hnsw_ef_construction, 200);
323        assert_eq!(cfg.index.hnsw_ef_search, 50);
324
325        assert_eq!(cfg.context.default_token_budget, 4096);
326        assert!((cfg.context.token_multiplier - 1.3).abs() < f32::EPSILON);
327        assert!((cfg.context.zone_system_pct - 0.10).abs() < f32::EPSILON);
328        assert!((cfg.context.zone_critical_pct - 0.25).abs() < f32::EPSILON);
329        assert!((cfg.context.zone_primary_pct - 0.35).abs() < f32::EPSILON);
330        assert!((cfg.context.zone_supporting_pct - 0.20).abs() < f32::EPSILON);
331        assert!((cfg.context.zone_reference_pct - 0.10).abs() < f32::EPSILON);
332
333        assert!((cfg.cognitive.contradiction_threshold - 0.95).abs() < f32::EPSILON);
334        assert!((cfg.cognitive.related_threshold_min - 0.6).abs() < f32::EPSILON);
335        assert!((cfg.cognitive.related_threshold_max - 0.85).abs() < f32::EPSILON);
336        assert!((cfg.cognitive.interference_threshold - 0.8).abs() < f32::EPSILON);
337        assert_eq!(cfg.cognitive.speculative_cache_size, 10);
338        assert!((cfg.cognitive.speculative_hit_threshold - 0.5).abs() < f32::EPSILON);
339        assert_eq!(cfg.cognitive.max_trajectory_turns, 100);
340        assert_eq!(cfg.cognitive.max_pain_warnings, 5);
341        assert_eq!(cfg.cognitive.max_phantom_warnings, 5);
342
343        assert!((cfg.consolidation.decay_half_life_hours - 168.0).abs() < f64::EPSILON);
344        assert!((cfg.consolidation.min_salience - 0.01).abs() < f32::EPSILON);
345        assert_eq!(cfg.consolidation.archival_min_age_days, 30);
346        assert!((cfg.consolidation.archival_max_salience - 0.05).abs() < f32::EPSILON);
347
348        assert_eq!(cfg.server.host, "0.0.0.0");
349        assert_eq!(cfg.server.port, 6677);
350    }
351
352    #[test]
353    fn test_from_file() {
354        let dir = std::env::temp_dir().join("mentedb_config_test");
355        std::fs::create_dir_all(&dir).unwrap();
356        let path = dir.join("config.json");
357
358        let json = r#"{
359            "storage": {
360                "data_dir": "/var/mentedb",
361                "buffer_pool_size": 2048,
362                "page_size": 8192
363            },
364            "index": {
365                "hnsw_m": 32,
366                "hnsw_ef_construction": 400,
367                "hnsw_ef_search": 100
368            },
369            "server": {
370                "host": "127.0.0.1",
371                "port": 9999
372            }
373        }"#;
374        std::fs::write(&path, json).unwrap();
375
376        let cfg = MenteConfig::from_file(&path).unwrap();
377
378        assert_eq!(cfg.storage.data_dir, "/var/mentedb");
379        assert_eq!(cfg.storage.buffer_pool_size, 2048);
380        assert_eq!(cfg.storage.page_size, 8192);
381        assert_eq!(cfg.index.hnsw_m, 32);
382        assert_eq!(cfg.index.hnsw_ef_construction, 400);
383        assert_eq!(cfg.index.hnsw_ef_search, 100);
384        assert_eq!(cfg.server.host, "127.0.0.1");
385        assert_eq!(cfg.server.port, 9999);
386
387        // Sections not provided should use defaults.
388        assert_eq!(cfg.context.default_token_budget, 4096);
389        assert!((cfg.cognitive.contradiction_threshold - 0.95).abs() < f32::EPSILON);
390        assert!((cfg.consolidation.decay_half_life_hours - 168.0).abs() < f64::EPSILON);
391
392        std::fs::remove_dir_all(&dir).ok();
393    }
394
395    #[test]
396    fn test_from_file_empty_object() {
397        let dir = std::env::temp_dir().join("mentedb_config_test_empty");
398        std::fs::create_dir_all(&dir).unwrap();
399        let path = dir.join("config.json");
400        std::fs::write(&path, "{}").unwrap();
401
402        let cfg = MenteConfig::from_file(&path).unwrap();
403        let defaults = MenteConfig::default();
404
405        assert_eq!(
406            cfg.storage.buffer_pool_size,
407            defaults.storage.buffer_pool_size
408        );
409        assert_eq!(cfg.index.hnsw_m, defaults.index.hnsw_m);
410        assert_eq!(cfg.server.port, defaults.server.port);
411
412        std::fs::remove_dir_all(&dir).ok();
413    }
414
415    #[test]
416    fn test_from_file_not_found() {
417        let result = MenteConfig::from_file(Path::new("/nonexistent/config.json"));
418        assert!(result.is_err());
419    }
420
421    #[test]
422    fn test_from_file_invalid_json() {
423        let dir = std::env::temp_dir().join("mentedb_config_test_invalid");
424        std::fs::create_dir_all(&dir).unwrap();
425        let path = dir.join("config.json");
426        std::fs::write(&path, "not json at all").unwrap();
427
428        let result = MenteConfig::from_file(&path);
429        assert!(result.is_err());
430
431        std::fs::remove_dir_all(&dir).ok();
432    }
433}