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_buffer_pool_size")]
38 pub buffer_pool_size: usize,
39 #[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#[derive(Debug, Clone, Deserialize)]
63pub struct IndexConfig {
64 #[serde(default = "default_hnsw_m")]
66 pub hnsw_m: usize,
67 #[serde(default = "default_hnsw_ef_construction")]
69 pub hnsw_ef_construction: usize,
70 #[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#[derive(Debug, Clone, Deserialize)]
97pub struct ContextConfig {
98 #[serde(default = "default_token_budget")]
100 pub default_token_budget: usize,
101 #[serde(default = "default_token_multiplier")]
103 pub token_multiplier: f32,
104 #[serde(default = "default_zone_system_pct")]
106 pub zone_system_pct: f32,
107 #[serde(default = "default_zone_critical_pct")]
109 pub zone_critical_pct: f32,
110 #[serde(default = "default_zone_primary_pct")]
112 pub zone_primary_pct: f32,
113 #[serde(default = "default_zone_supporting_pct")]
115 pub zone_supporting_pct: f32,
116 #[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#[derive(Debug, Clone, Deserialize)]
159pub struct CognitiveConfig {
160 #[serde(default = "default_contradiction_threshold")]
162 pub contradiction_threshold: f32,
163 #[serde(default = "default_related_threshold_min")]
165 pub related_threshold_min: f32,
166 #[serde(default = "default_related_threshold_max")]
168 pub related_threshold_max: f32,
169 #[serde(default = "default_interference_threshold")]
171 pub interference_threshold: f32,
172 #[serde(default = "default_speculative_cache_size")]
174 pub speculative_cache_size: usize,
175 #[serde(default = "default_speculative_hit_threshold")]
177 pub speculative_hit_threshold: f32,
178 #[serde(default = "default_max_trajectory_turns")]
180 pub max_trajectory_turns: usize,
181 #[serde(default = "default_max_pain_warnings")]
183 pub max_pain_warnings: usize,
184 #[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#[derive(Debug, Clone, Deserialize)]
235pub struct ConsolidationConfig {
236 #[serde(default = "default_decay_half_life_hours")]
238 pub decay_half_life_hours: f64,
239 #[serde(default = "default_min_salience")]
241 pub min_salience: f32,
242 #[serde(default = "default_archival_min_age_days")]
244 pub archival_min_age_days: u64,
245 #[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#[derive(Debug, Clone, Deserialize)]
276pub struct ServerConfig {
277 #[serde(default = "default_host")]
279 pub host: String,
280 #[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 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 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}