1use serde::Deserialize;
4use std::path::Path;
5
6use crate::error::{MenteError, MenteResult};
7
8#[derive(Debug, Clone, Deserialize, Default)]
10pub struct MenteConfig {
11 #[serde(default)]
13 pub storage: StorageConfig,
14 #[serde(default)]
16 pub index: IndexConfig,
17 #[serde(default)]
19 pub context: ContextConfig,
20 #[serde(default)]
22 pub cognitive: CognitiveConfig,
23 #[serde(default)]
25 pub consolidation: ConsolidationConfig,
26 #[serde(default)]
28 pub server: ServerConfig,
29}
30
31#[derive(Debug, Clone, Deserialize)]
33pub struct StorageConfig {
34 pub data_dir: String,
36 #[serde(default = "default_cache_bytes")]
42 pub cache_bytes: u64,
43 #[serde(default = "default_wal_checkpoint_bytes")]
46 pub wal_checkpoint_bytes: u64,
47 #[serde(default = "default_max_record_bytes")]
50 pub max_record_bytes: u64,
51}
52
53fn default_cache_bytes() -> u64 {
54 64 * 1024 * 1024
55}
56fn default_wal_checkpoint_bytes() -> u64 {
57 8 * 1024 * 1024
58}
59fn default_max_record_bytes() -> u64 {
60 16 * 1024 * 1024
61}
62
63impl Default for StorageConfig {
64 fn default() -> Self {
65 Self {
66 data_dir: "data".to_string(),
67 cache_bytes: default_cache_bytes(),
68 wal_checkpoint_bytes: default_wal_checkpoint_bytes(),
69 max_record_bytes: default_max_record_bytes(),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Deserialize)]
76pub struct IndexConfig {
77 #[serde(default = "default_hnsw_m")]
79 pub hnsw_m: usize,
80 #[serde(default = "default_hnsw_ef_construction")]
82 pub hnsw_ef_construction: usize,
83 #[serde(default = "default_hnsw_ef_search")]
85 pub hnsw_ef_search: usize,
86}
87
88fn default_hnsw_m() -> usize {
89 16
90}
91fn default_hnsw_ef_construction() -> usize {
92 200
93}
94fn default_hnsw_ef_search() -> usize {
95 50
96}
97
98impl Default for IndexConfig {
99 fn default() -> Self {
100 Self {
101 hnsw_m: default_hnsw_m(),
102 hnsw_ef_construction: default_hnsw_ef_construction(),
103 hnsw_ef_search: default_hnsw_ef_search(),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Deserialize)]
110pub struct ContextConfig {
111 #[serde(default = "default_token_budget")]
113 pub default_token_budget: usize,
114 #[serde(default = "default_token_multiplier")]
116 pub token_multiplier: f32,
117 #[serde(default = "default_zone_system_pct")]
119 pub zone_system_pct: f32,
120 #[serde(default = "default_zone_critical_pct")]
122 pub zone_critical_pct: f32,
123 #[serde(default = "default_zone_primary_pct")]
125 pub zone_primary_pct: f32,
126 #[serde(default = "default_zone_supporting_pct")]
128 pub zone_supporting_pct: f32,
129 #[serde(default = "default_zone_reference_pct")]
131 pub zone_reference_pct: f32,
132}
133
134fn default_token_budget() -> usize {
135 4096
136}
137fn default_token_multiplier() -> f32 {
138 1.3
139}
140fn default_zone_system_pct() -> f32 {
141 0.10
142}
143fn default_zone_critical_pct() -> f32 {
144 0.25
145}
146fn default_zone_primary_pct() -> f32 {
147 0.35
148}
149fn default_zone_supporting_pct() -> f32 {
150 0.20
151}
152fn default_zone_reference_pct() -> f32 {
153 0.10
154}
155
156impl Default for ContextConfig {
157 fn default() -> Self {
158 Self {
159 default_token_budget: default_token_budget(),
160 token_multiplier: default_token_multiplier(),
161 zone_system_pct: default_zone_system_pct(),
162 zone_critical_pct: default_zone_critical_pct(),
163 zone_primary_pct: default_zone_primary_pct(),
164 zone_supporting_pct: default_zone_supporting_pct(),
165 zone_reference_pct: default_zone_reference_pct(),
166 }
167 }
168}
169
170#[derive(Debug, Clone, Deserialize)]
172pub struct CognitiveConfig {
173 #[serde(default = "default_contradiction_threshold")]
175 pub contradiction_threshold: f32,
176 #[serde(default = "default_related_threshold_min")]
178 pub related_threshold_min: f32,
179 #[serde(default = "default_related_threshold_max")]
181 pub related_threshold_max: f32,
182 #[serde(default = "default_interference_threshold")]
184 pub interference_threshold: f32,
185 #[serde(default = "default_speculative_cache_size")]
187 pub speculative_cache_size: usize,
188 #[serde(default = "default_speculative_hit_threshold")]
190 pub speculative_hit_threshold: f32,
191 #[serde(default = "default_max_trajectory_turns")]
193 pub max_trajectory_turns: usize,
194 #[serde(default = "default_max_pain_warnings")]
196 pub max_pain_warnings: usize,
197 #[serde(default = "default_max_phantom_warnings")]
199 pub max_phantom_warnings: usize,
200}
201
202fn default_contradiction_threshold() -> f32 {
203 0.95
204}
205fn default_related_threshold_min() -> f32 {
206 0.6
207}
208fn default_related_threshold_max() -> f32 {
209 0.85
210}
211fn default_interference_threshold() -> f32 {
212 0.8
213}
214fn default_speculative_cache_size() -> usize {
215 10
216}
217fn default_speculative_hit_threshold() -> f32 {
218 0.5
219}
220fn default_max_trajectory_turns() -> usize {
221 100
222}
223fn default_max_pain_warnings() -> usize {
224 5
225}
226fn default_max_phantom_warnings() -> usize {
227 5
228}
229
230impl Default for CognitiveConfig {
231 fn default() -> Self {
232 Self {
233 contradiction_threshold: default_contradiction_threshold(),
234 related_threshold_min: default_related_threshold_min(),
235 related_threshold_max: default_related_threshold_max(),
236 interference_threshold: default_interference_threshold(),
237 speculative_cache_size: default_speculative_cache_size(),
238 speculative_hit_threshold: default_speculative_hit_threshold(),
239 max_trajectory_turns: default_max_trajectory_turns(),
240 max_pain_warnings: default_max_pain_warnings(),
241 max_phantom_warnings: default_max_phantom_warnings(),
242 }
243 }
244}
245
246#[derive(Debug, Clone, Deserialize)]
248pub struct ConsolidationConfig {
249 #[serde(default = "default_decay_half_life_hours")]
251 pub decay_half_life_hours: f64,
252 #[serde(default = "default_min_salience")]
254 pub min_salience: f32,
255 #[serde(default = "default_archival_min_age_days")]
257 pub archival_min_age_days: u64,
258 #[serde(default = "default_archival_max_salience")]
260 pub archival_max_salience: f32,
261}
262
263fn default_decay_half_life_hours() -> f64 {
264 168.0
265}
266fn default_min_salience() -> f32 {
267 0.01
268}
269fn default_archival_min_age_days() -> u64 {
270 30
271}
272fn default_archival_max_salience() -> f32 {
273 0.05
274}
275
276impl Default for ConsolidationConfig {
277 fn default() -> Self {
278 Self {
279 decay_half_life_hours: default_decay_half_life_hours(),
280 min_salience: default_min_salience(),
281 archival_min_age_days: default_archival_min_age_days(),
282 archival_max_salience: default_archival_max_salience(),
283 }
284 }
285}
286
287#[derive(Debug, Clone, Deserialize)]
289pub struct ServerConfig {
290 #[serde(default = "default_host")]
292 pub host: String,
293 #[serde(default = "default_port")]
295 pub port: u16,
296}
297
298fn default_host() -> String {
299 "0.0.0.0".to_string()
300}
301fn default_port() -> u16 {
302 6677
303}
304
305impl Default for ServerConfig {
306 fn default() -> Self {
307 Self {
308 host: default_host(),
309 port: default_port(),
310 }
311 }
312}
313
314impl MenteConfig {
315 pub fn from_file(path: &Path) -> MenteResult<Self> {
317 let contents = std::fs::read_to_string(path).map_err(MenteError::Io)?;
318 serde_json::from_str(&contents).map_err(|e| MenteError::Serialization(e.to_string()))
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_defaults() {
328 let cfg = MenteConfig::default();
329
330 assert_eq!(cfg.storage.data_dir, "data");
331 assert_eq!(cfg.storage.cache_bytes, 64 * 1024 * 1024);
332 assert_eq!(cfg.storage.wal_checkpoint_bytes, 8 * 1024 * 1024);
333 assert_eq!(cfg.storage.max_record_bytes, 16 * 1024 * 1024);
334
335 assert_eq!(cfg.index.hnsw_m, 16);
336 assert_eq!(cfg.index.hnsw_ef_construction, 200);
337 assert_eq!(cfg.index.hnsw_ef_search, 50);
338
339 assert_eq!(cfg.context.default_token_budget, 4096);
340 assert!((cfg.context.token_multiplier - 1.3).abs() < f32::EPSILON);
341 assert!((cfg.context.zone_system_pct - 0.10).abs() < f32::EPSILON);
342 assert!((cfg.context.zone_critical_pct - 0.25).abs() < f32::EPSILON);
343 assert!((cfg.context.zone_primary_pct - 0.35).abs() < f32::EPSILON);
344 assert!((cfg.context.zone_supporting_pct - 0.20).abs() < f32::EPSILON);
345 assert!((cfg.context.zone_reference_pct - 0.10).abs() < f32::EPSILON);
346
347 assert!((cfg.cognitive.contradiction_threshold - 0.95).abs() < f32::EPSILON);
348 assert!((cfg.cognitive.related_threshold_min - 0.6).abs() < f32::EPSILON);
349 assert!((cfg.cognitive.related_threshold_max - 0.85).abs() < f32::EPSILON);
350 assert!((cfg.cognitive.interference_threshold - 0.8).abs() < f32::EPSILON);
351 assert_eq!(cfg.cognitive.speculative_cache_size, 10);
352 assert!((cfg.cognitive.speculative_hit_threshold - 0.5).abs() < f32::EPSILON);
353 assert_eq!(cfg.cognitive.max_trajectory_turns, 100);
354 assert_eq!(cfg.cognitive.max_pain_warnings, 5);
355 assert_eq!(cfg.cognitive.max_phantom_warnings, 5);
356
357 assert!((cfg.consolidation.decay_half_life_hours - 168.0).abs() < f64::EPSILON);
358 assert!((cfg.consolidation.min_salience - 0.01).abs() < f32::EPSILON);
359 assert_eq!(cfg.consolidation.archival_min_age_days, 30);
360 assert!((cfg.consolidation.archival_max_salience - 0.05).abs() < f32::EPSILON);
361
362 assert_eq!(cfg.server.host, "0.0.0.0");
363 assert_eq!(cfg.server.port, 6677);
364 }
365
366 #[test]
367 fn test_from_file() {
368 let dir = std::env::temp_dir().join("mentedb_config_test");
369 std::fs::create_dir_all(&dir).unwrap();
370 let path = dir.join("config.json");
371
372 let json = r#"{
373 "storage": {
374 "data_dir": "/var/mentedb",
375 "cache_bytes": 2147483648,
376 "buffer_pool_size": 2048
377 },
378 "index": {
379 "hnsw_m": 32,
380 "hnsw_ef_construction": 400,
381 "hnsw_ef_search": 100
382 },
383 "server": {
384 "host": "127.0.0.1",
385 "port": 9999
386 }
387 }"#;
388 std::fs::write(&path, json).unwrap();
389
390 let cfg = MenteConfig::from_file(&path).unwrap();
391
392 assert_eq!(cfg.storage.data_dir, "/var/mentedb");
393 assert_eq!(cfg.storage.cache_bytes, 2 * 1024 * 1024 * 1024);
394 assert_eq!(cfg.storage.max_record_bytes, 16 * 1024 * 1024);
396 assert_eq!(cfg.index.hnsw_m, 32);
397 assert_eq!(cfg.index.hnsw_ef_construction, 400);
398 assert_eq!(cfg.index.hnsw_ef_search, 100);
399 assert_eq!(cfg.server.host, "127.0.0.1");
400 assert_eq!(cfg.server.port, 9999);
401
402 assert_eq!(cfg.context.default_token_budget, 4096);
404 assert!((cfg.cognitive.contradiction_threshold - 0.95).abs() < f32::EPSILON);
405 assert!((cfg.consolidation.decay_half_life_hours - 168.0).abs() < f64::EPSILON);
406
407 std::fs::remove_dir_all(&dir).ok();
408 }
409
410 #[test]
411 fn test_from_file_empty_object() {
412 let dir = std::env::temp_dir().join("mentedb_config_test_empty");
413 std::fs::create_dir_all(&dir).unwrap();
414 let path = dir.join("config.json");
415 std::fs::write(&path, "{}").unwrap();
416
417 let cfg = MenteConfig::from_file(&path).unwrap();
418 let defaults = MenteConfig::default();
419
420 assert_eq!(cfg.storage.cache_bytes, defaults.storage.cache_bytes);
421 assert_eq!(cfg.index.hnsw_m, defaults.index.hnsw_m);
422 assert_eq!(cfg.server.port, defaults.server.port);
423
424 std::fs::remove_dir_all(&dir).ok();
425 }
426
427 #[test]
428 fn test_from_file_not_found() {
429 let result = MenteConfig::from_file(Path::new("/nonexistent/config.json"));
430 assert!(result.is_err());
431 }
432
433 #[test]
434 fn test_from_file_invalid_json() {
435 let dir = std::env::temp_dir().join("mentedb_config_test_invalid");
436 std::fs::create_dir_all(&dir).unwrap();
437 let path = dir.join("config.json");
438 std::fs::write(&path, "not json at all").unwrap();
439
440 let result = MenteConfig::from_file(&path);
441 assert!(result.is_err());
442
443 std::fs::remove_dir_all(&dir).ok();
444 }
445}