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")]
41 pub buffer_pool_size: usize,
42 #[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#[derive(Debug, Clone, Deserialize)]
69pub struct IndexConfig {
70 #[serde(default = "default_hnsw_m")]
72 pub hnsw_m: usize,
73 #[serde(default = "default_hnsw_ef_construction")]
75 pub hnsw_ef_construction: usize,
76 #[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#[derive(Debug, Clone, Deserialize)]
103pub struct ContextConfig {
104 #[serde(default = "default_token_budget")]
106 pub default_token_budget: usize,
107 #[serde(default = "default_token_multiplier")]
109 pub token_multiplier: f32,
110 #[serde(default = "default_zone_system_pct")]
112 pub zone_system_pct: f32,
113 #[serde(default = "default_zone_critical_pct")]
115 pub zone_critical_pct: f32,
116 #[serde(default = "default_zone_primary_pct")]
118 pub zone_primary_pct: f32,
119 #[serde(default = "default_zone_supporting_pct")]
121 pub zone_supporting_pct: f32,
122 #[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#[derive(Debug, Clone, Deserialize)]
165pub struct CognitiveConfig {
166 #[serde(default = "default_contradiction_threshold")]
168 pub contradiction_threshold: f32,
169 #[serde(default = "default_related_threshold_min")]
171 pub related_threshold_min: f32,
172 #[serde(default = "default_related_threshold_max")]
174 pub related_threshold_max: f32,
175 #[serde(default = "default_interference_threshold")]
177 pub interference_threshold: f32,
178 #[serde(default = "default_speculative_cache_size")]
180 pub speculative_cache_size: usize,
181 #[serde(default = "default_speculative_hit_threshold")]
183 pub speculative_hit_threshold: f32,
184 #[serde(default = "default_max_trajectory_turns")]
186 pub max_trajectory_turns: usize,
187 #[serde(default = "default_max_pain_warnings")]
189 pub max_pain_warnings: usize,
190 #[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#[derive(Debug, Clone, Deserialize)]
241pub struct ConsolidationConfig {
242 #[serde(default = "default_decay_half_life_hours")]
244 pub decay_half_life_hours: f64,
245 #[serde(default = "default_min_salience")]
247 pub min_salience: f32,
248 #[serde(default = "default_archival_min_age_days")]
250 pub archival_min_age_days: u64,
251 #[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#[derive(Debug, Clone, Deserialize)]
282pub struct ServerConfig {
283 #[serde(default = "default_host")]
285 pub host: String,
286 #[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 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 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}