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