1use std::sync::Arc;
23
24use arrow::array::{Array, Float64Array, RecordBatch, StringArray, UInt8Array};
25use arrow::datatypes::{DataType, Field, Schema};
26
27pub const COGNITIVE_PARAMS_SCHEMA_VERSION: &str = "1.0.0";
31
32pub mod param_col {
34 pub const PARAM_ID: usize = 0;
35 pub const CATEGORY: usize = 1;
36 pub const VALUE_F64: usize = 2;
37 pub const VALUE_STR: usize = 3;
38 pub const MIN_BOUND: usize = 4;
39 pub const MAX_BOUND: usize = 5;
40 pub const AUTONOMY_TIER: usize = 6;
41 pub const MODIFIED_BY: usize = 7;
42}
43
44pub mod category {
46 pub const SIGNAL_WEIGHT: &str = "signal_weight";
47 pub const THRESHOLD: &str = "threshold";
48 pub const LEARNING_CONFIG: &str = "learning_config";
49 pub const TRIGGER: &str = "trigger";
50 pub const LOSS_COEFFICIENT: &str = "loss_coefficient";
51 pub const CONSOLIDATION_TRIGGER: &str = "consolidation_trigger";
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
56#[repr(u8)]
57pub enum AutonomyTier {
58 Auto = 1,
60 BeingApproved = 2,
62 CaptainOnly = 3,
64}
65
66impl AutonomyTier {
67 pub fn from_u8(v: u8) -> Option<Self> {
68 match v {
69 1 => Some(Self::Auto),
70 2 => Some(Self::BeingApproved),
71 3 => Some(Self::CaptainOnly),
72 _ => None,
73 }
74 }
75}
76
77pub fn cognitive_params_schema() -> Schema {
81 Schema::new(vec![
82 Field::new("param_id", DataType::Utf8, false),
83 Field::new("category", DataType::Utf8, false),
84 Field::new("value_f64", DataType::Float64, true),
85 Field::new("value_str", DataType::Utf8, true),
86 Field::new("min_bound", DataType::Float64, true),
87 Field::new("max_bound", DataType::Float64, true),
88 Field::new("autonomy_tier", DataType::UInt8, false),
89 Field::new("modified_by", DataType::Utf8, false),
90 ])
91}
92
93#[derive(Debug, Clone, PartialEq)]
97pub struct CognitiveParameter {
98 pub param_id: String,
99 pub category: String,
100 pub value_f64: Option<f64>,
101 pub value_str: Option<String>,
102 pub min_bound: Option<f64>,
103 pub max_bound: Option<f64>,
104 pub autonomy_tier: AutonomyTier,
105 pub modified_by: String,
106}
107
108#[derive(Debug, thiserror::Error)]
111pub enum ParamStoreError {
112 #[error("parameter not found: {0}")]
113 NotFound(String),
114
115 #[error("value {value} out of bounds [{min}, {max}] for parameter {param_id}")]
116 OutOfBounds {
117 param_id: String,
118 value: f64,
119 min: f64,
120 max: f64,
121 },
122
123 #[error(
124 "autonomy tier violation: parameter {param_id} requires tier {required:?}, caller is {caller:?}"
125 )]
126 TierViolation {
127 param_id: String,
128 required: AutonomyTier,
129 caller: AutonomyTier,
130 },
131
132 #[error("arrow error: {0}")]
133 Arrow(#[from] arrow::error::ArrowError),
134
135 #[error("parquet error: {0}")]
136 Parquet(#[from] parquet::errors::ParquetError),
137
138 #[error("invalid schema: {0}")]
139 InvalidSchema(String),
140}
141
142#[derive(Debug, Clone)]
150pub struct CognitiveParameterStore {
151 batch: RecordBatch,
152}
153
154impl CognitiveParameterStore {
155 pub fn new() -> Self {
157 let schema = Arc::new(cognitive_params_schema());
158 let batch = RecordBatch::new_empty(schema);
159 Self { batch }
160 }
161
162 pub fn from_batch(batch: RecordBatch) -> Result<Self, ParamStoreError> {
166 let expected = cognitive_params_schema();
167 if batch.schema().fields().len() != expected.fields().len() {
168 return Err(ParamStoreError::InvalidSchema(format!(
169 "expected {} columns, got {}",
170 expected.fields().len(),
171 batch.schema().fields().len()
172 )));
173 }
174 let actual_schema = batch.schema();
175 for (i, field) in expected.fields().iter().enumerate() {
176 let actual = actual_schema.field(i);
177 if actual.data_type() != field.data_type() {
178 return Err(ParamStoreError::InvalidSchema(format!(
179 "column {}: expected {:?}, got {:?}",
180 i,
181 field.data_type(),
182 actual.data_type()
183 )));
184 }
185 }
186 Ok(Self { batch })
187 }
188
189 pub fn len(&self) -> usize {
191 self.batch.num_rows()
192 }
193
194 pub fn is_empty(&self) -> bool {
196 self.batch.num_rows() == 0
197 }
198
199 pub fn get(&self, param_id: &str) -> Option<CognitiveParameter> {
201 let ids = self.param_id_col();
202 for i in 0..self.batch.num_rows() {
203 if ids.value(i) == param_id {
204 return Some(self.row_to_param(i));
205 }
206 }
207 None
208 }
209
210 pub fn list(&self, cat: &str) -> Vec<CognitiveParameter> {
212 let categories = self.category_col();
213 let mut result = Vec::new();
214 for i in 0..self.batch.num_rows() {
215 if categories.value(i) == cat {
216 result.push(self.row_to_param(i));
217 }
218 }
219 result
220 }
221
222 pub fn list_all(&self) -> Vec<CognitiveParameter> {
224 (0..self.batch.num_rows())
225 .map(|i| self.row_to_param(i))
226 .collect()
227 }
228
229 pub fn set(
234 &mut self,
235 param_id: &str,
236 value: f64,
237 modified_by: &str,
238 caller_tier: AutonomyTier,
239 ) -> Result<(), ParamStoreError> {
240 let idx = self.find_index(param_id)?;
241 let param = self.row_to_param(idx);
242
243 if caller_tier < param.autonomy_tier {
245 return Err(ParamStoreError::TierViolation {
246 param_id: param_id.to_string(),
247 required: param.autonomy_tier,
248 caller: caller_tier,
249 });
250 }
251
252 if let (Some(min), Some(max)) = (param.min_bound, param.max_bound)
254 && (value < min || value > max)
255 {
256 return Err(ParamStoreError::OutOfBounds {
257 param_id: param_id.to_string(),
258 value,
259 min,
260 max,
261 });
262 }
263
264 self.batch = self.rebuild_with_f64_update(idx, value, modified_by)?;
266 Ok(())
267 }
268
269 pub fn insert(&mut self, param: &CognitiveParameter) -> Result<(), ParamStoreError> {
271 let existing_idx = {
273 let ids = self.param_id_col();
274 (0..self.batch.num_rows()).find(|&i| ids.value(i) == param.param_id)
275 };
276
277 let batch = if let Some(idx) = existing_idx {
278 self.remove_row(idx)?
279 } else {
280 self.batch.clone()
281 };
282
283 self.batch = append_row(&batch, param)?;
285 Ok(())
286 }
287
288 pub fn snapshot(&self) -> &RecordBatch {
290 &self.batch
291 }
292
293 pub fn into_batch(self) -> RecordBatch {
295 self.batch
296 }
297
298 fn param_id_col(&self) -> &StringArray {
301 self.batch
302 .column(param_col::PARAM_ID)
303 .as_any()
304 .downcast_ref::<StringArray>()
305 .expect("param_id column is StringArray")
306 }
307
308 fn category_col(&self) -> &StringArray {
309 self.batch
310 .column(param_col::CATEGORY)
311 .as_any()
312 .downcast_ref::<StringArray>()
313 .expect("category column is StringArray")
314 }
315
316 fn find_index(&self, param_id: &str) -> Result<usize, ParamStoreError> {
317 let ids = self.param_id_col();
318 for i in 0..self.batch.num_rows() {
319 if ids.value(i) == param_id {
320 return Ok(i);
321 }
322 }
323 Err(ParamStoreError::NotFound(param_id.to_string()))
324 }
325
326 fn row_to_param(&self, idx: usize) -> CognitiveParameter {
327 let ids = self.param_id_col();
328 let categories = self.category_col();
329 let values_f64 = self
330 .batch
331 .column(param_col::VALUE_F64)
332 .as_any()
333 .downcast_ref::<Float64Array>()
334 .expect("value_f64 column");
335 let values_str = self
336 .batch
337 .column(param_col::VALUE_STR)
338 .as_any()
339 .downcast_ref::<StringArray>()
340 .expect("value_str column");
341 let min_bounds = self
342 .batch
343 .column(param_col::MIN_BOUND)
344 .as_any()
345 .downcast_ref::<Float64Array>()
346 .expect("min_bound column");
347 let max_bounds = self
348 .batch
349 .column(param_col::MAX_BOUND)
350 .as_any()
351 .downcast_ref::<Float64Array>()
352 .expect("max_bound column");
353 let tiers = self
354 .batch
355 .column(param_col::AUTONOMY_TIER)
356 .as_any()
357 .downcast_ref::<UInt8Array>()
358 .expect("autonomy_tier column");
359 let modified = self
360 .batch
361 .column(param_col::MODIFIED_BY)
362 .as_any()
363 .downcast_ref::<StringArray>()
364 .expect("modified_by column");
365
366 CognitiveParameter {
367 param_id: ids.value(idx).to_string(),
368 category: categories.value(idx).to_string(),
369 value_f64: if values_f64.is_null(idx) {
370 None
371 } else {
372 Some(values_f64.value(idx))
373 },
374 value_str: if values_str.is_null(idx) {
375 None
376 } else {
377 Some(values_str.value(idx).to_string())
378 },
379 min_bound: if min_bounds.is_null(idx) {
380 None
381 } else {
382 Some(min_bounds.value(idx))
383 },
384 max_bound: if max_bounds.is_null(idx) {
385 None
386 } else {
387 Some(max_bounds.value(idx))
388 },
389 autonomy_tier: AutonomyTier::from_u8(tiers.value(idx))
390 .unwrap_or(AutonomyTier::CaptainOnly),
391 modified_by: modified.value(idx).to_string(),
392 }
393 }
394
395 fn rebuild_with_f64_update(
396 &self,
397 idx: usize,
398 new_value: f64,
399 new_modified_by: &str,
400 ) -> Result<RecordBatch, ParamStoreError> {
401 let n = self.batch.num_rows();
402
403 let ids = self.param_id_col();
405 let cats = self.category_col();
406 let vals = self
407 .batch
408 .column(param_col::VALUE_F64)
409 .as_any()
410 .downcast_ref::<Float64Array>()
411 .expect("value_f64");
412 let strs = self
413 .batch
414 .column(param_col::VALUE_STR)
415 .as_any()
416 .downcast_ref::<StringArray>()
417 .expect("value_str");
418 let mins = self
419 .batch
420 .column(param_col::MIN_BOUND)
421 .as_any()
422 .downcast_ref::<Float64Array>()
423 .expect("min_bound");
424 let maxs = self
425 .batch
426 .column(param_col::MAX_BOUND)
427 .as_any()
428 .downcast_ref::<Float64Array>()
429 .expect("max_bound");
430 let tiers = self
431 .batch
432 .column(param_col::AUTONOMY_TIER)
433 .as_any()
434 .downcast_ref::<UInt8Array>()
435 .expect("autonomy_tier");
436 let mods = self
437 .batch
438 .column(param_col::MODIFIED_BY)
439 .as_any()
440 .downcast_ref::<StringArray>()
441 .expect("modified_by");
442
443 let new_ids: Vec<&str> = (0..n).map(|i| ids.value(i)).collect();
444 let new_cats: Vec<&str> = (0..n).map(|i| cats.value(i)).collect();
445 let new_vals: Vec<Option<f64>> = (0..n)
446 .map(|i| {
447 if i == idx {
448 Some(new_value)
449 } else if vals.is_null(i) {
450 None
451 } else {
452 Some(vals.value(i))
453 }
454 })
455 .collect();
456 let new_strs: Vec<Option<&str>> = (0..n)
457 .map(|i| {
458 if strs.is_null(i) {
459 None
460 } else {
461 Some(strs.value(i))
462 }
463 })
464 .collect();
465 let new_mins: Vec<Option<f64>> = (0..n)
466 .map(|i| {
467 if mins.is_null(i) {
468 None
469 } else {
470 Some(mins.value(i))
471 }
472 })
473 .collect();
474 let new_maxs: Vec<Option<f64>> = (0..n)
475 .map(|i| {
476 if maxs.is_null(i) {
477 None
478 } else {
479 Some(maxs.value(i))
480 }
481 })
482 .collect();
483 let new_tiers: Vec<u8> = (0..n).map(|i| tiers.value(i)).collect();
484 let new_mods: Vec<&str> = (0..n)
485 .map(|i| {
486 if i == idx {
487 new_modified_by
488 } else {
489 mods.value(i)
490 }
491 })
492 .collect();
493
494 Ok(RecordBatch::try_new(
495 Arc::new(cognitive_params_schema()),
496 vec![
497 Arc::new(StringArray::from(new_ids)),
498 Arc::new(StringArray::from(new_cats)),
499 Arc::new(Float64Array::from(new_vals)),
500 Arc::new(StringArray::from(new_strs)),
501 Arc::new(Float64Array::from(new_mins)),
502 Arc::new(Float64Array::from(new_maxs)),
503 Arc::new(UInt8Array::from(new_tiers)),
504 Arc::new(StringArray::from(new_mods)),
505 ],
506 )?)
507 }
508
509 fn remove_row(&self, idx: usize) -> Result<RecordBatch, ParamStoreError> {
510 let n = self.batch.num_rows();
511 if n == 0 {
512 return Ok(self.batch.clone());
513 }
514
515 let ids = self.param_id_col();
516 let cats = self.category_col();
517 let vals = self
518 .batch
519 .column(param_col::VALUE_F64)
520 .as_any()
521 .downcast_ref::<Float64Array>()
522 .expect("value_f64");
523 let strs = self
524 .batch
525 .column(param_col::VALUE_STR)
526 .as_any()
527 .downcast_ref::<StringArray>()
528 .expect("value_str");
529 let mins = self
530 .batch
531 .column(param_col::MIN_BOUND)
532 .as_any()
533 .downcast_ref::<Float64Array>()
534 .expect("min_bound");
535 let maxs = self
536 .batch
537 .column(param_col::MAX_BOUND)
538 .as_any()
539 .downcast_ref::<Float64Array>()
540 .expect("max_bound");
541 let tiers = self
542 .batch
543 .column(param_col::AUTONOMY_TIER)
544 .as_any()
545 .downcast_ref::<UInt8Array>()
546 .expect("autonomy_tier");
547 let mods = self
548 .batch
549 .column(param_col::MODIFIED_BY)
550 .as_any()
551 .downcast_ref::<StringArray>()
552 .expect("modified_by");
553
554 let keep: Vec<usize> = (0..n).filter(|&i| i != idx).collect();
555 if keep.is_empty() {
556 return Ok(RecordBatch::new_empty(Arc::new(cognitive_params_schema())));
557 }
558
559 let new_ids: Vec<&str> = keep.iter().map(|&i| ids.value(i)).collect();
560 let new_cats: Vec<&str> = keep.iter().map(|&i| cats.value(i)).collect();
561 let new_vals: Vec<Option<f64>> = keep
562 .iter()
563 .map(|&i| {
564 if vals.is_null(i) {
565 None
566 } else {
567 Some(vals.value(i))
568 }
569 })
570 .collect();
571 let new_strs: Vec<Option<&str>> = keep
572 .iter()
573 .map(|&i| {
574 if strs.is_null(i) {
575 None
576 } else {
577 Some(strs.value(i))
578 }
579 })
580 .collect();
581 let new_mins: Vec<Option<f64>> = keep
582 .iter()
583 .map(|&i| {
584 if mins.is_null(i) {
585 None
586 } else {
587 Some(mins.value(i))
588 }
589 })
590 .collect();
591 let new_maxs: Vec<Option<f64>> = keep
592 .iter()
593 .map(|&i| {
594 if maxs.is_null(i) {
595 None
596 } else {
597 Some(maxs.value(i))
598 }
599 })
600 .collect();
601 let new_tiers: Vec<u8> = keep.iter().map(|&i| tiers.value(i)).collect();
602 let new_mods: Vec<&str> = keep.iter().map(|&i| mods.value(i)).collect();
603
604 Ok(RecordBatch::try_new(
605 Arc::new(cognitive_params_schema()),
606 vec![
607 Arc::new(StringArray::from(new_ids)),
608 Arc::new(StringArray::from(new_cats)),
609 Arc::new(Float64Array::from(new_vals)),
610 Arc::new(StringArray::from(new_strs)),
611 Arc::new(Float64Array::from(new_mins)),
612 Arc::new(Float64Array::from(new_maxs)),
613 Arc::new(UInt8Array::from(new_tiers)),
614 Arc::new(StringArray::from(new_mods)),
615 ],
616 )?)
617 }
618
619 pub fn insert_signal_weight(
627 &mut self,
628 dimension: &str,
629 path: &str,
630 value: f64,
631 ) -> Result<(), ParamStoreError> {
632 let clipped = value.clamp(-1.5, 1.5);
633 let param = CognitiveParameter {
634 param_id: format!("sw.{dimension}.{path}"),
635 category: category::SIGNAL_WEIGHT.to_string(),
636 value_f64: Some(clipped),
637 value_str: None,
638 min_bound: Some(-1.5),
639 max_bound: Some(1.5),
640 autonomy_tier: AutonomyTier::Auto,
641 modified_by: "init".to_string(),
642 };
643 self.insert(¶m)
644 }
645
646 pub fn get_signal_weight(&self, dimension: &str, path: &str) -> Option<f64> {
648 let param_id = format!("sw.{dimension}.{path}");
649 self.get(¶m_id).and_then(|p| p.value_f64)
650 }
651
652 pub fn signal_weights(&self) -> Vec<(String, String, f64)> {
654 self.list(category::SIGNAL_WEIGHT)
655 .into_iter()
656 .filter_map(|p| {
657 if let Some(rest) = p.param_id.strip_prefix("sw.") {
658 if let Some(dot_pos) = rest.find('.') {
660 let dimension = rest[..dot_pos].to_string();
661 let path = rest[dot_pos + 1..].to_string();
662 return p.value_f64.map(|v| (dimension, path, v));
663 }
664 }
665 None
666 })
667 .collect()
668 }
669
670 pub fn signal_weight_count(&self) -> usize {
672 self.list(category::SIGNAL_WEIGHT)
673 .iter()
674 .filter(|p| p.param_id.starts_with("sw."))
675 .count()
676 }
677
678 pub fn insert_loss_coefficient(
685 &mut self,
686 name: &str,
687 value: f64,
688 ) -> Result<(), ParamStoreError> {
689 let param = CognitiveParameter {
690 param_id: format!("loss.{name}"),
691 category: category::LOSS_COEFFICIENT.to_string(),
692 value_f64: Some(value),
693 value_str: None,
694 min_bound: Some(0.0),
695 max_bound: Some(2.0),
696 autonomy_tier: AutonomyTier::Auto,
697 modified_by: "init".to_string(),
698 };
699 self.insert(¶m)
700 }
701
702 pub fn get_loss_coefficient(&self, name: &str) -> Option<f64> {
704 let param_id = format!("loss.{name}");
705 self.get(¶m_id).and_then(|p| p.value_f64)
706 }
707
708 pub fn loss_coefficients(&self) -> Vec<(String, f64)> {
710 self.list(category::LOSS_COEFFICIENT)
711 .into_iter()
712 .filter_map(|p| {
713 let name = p.param_id.strip_prefix("loss.")?.to_string();
714 p.value_f64.map(|v| (name, v))
715 })
716 .collect()
717 }
718
719 pub fn insert_consolidation_trigger(
726 &mut self,
727 name: &str,
728 value: f64,
729 ) -> Result<(), ParamStoreError> {
730 let param = CognitiveParameter {
731 param_id: format!("consol.{name}"),
732 category: category::CONSOLIDATION_TRIGGER.to_string(),
733 value_f64: Some(value),
734 value_str: None,
735 min_bound: Some(0.0),
736 max_bound: None,
737 autonomy_tier: AutonomyTier::BeingApproved,
738 modified_by: "init".to_string(),
739 };
740 self.insert(¶m)
741 }
742
743 pub fn get_consolidation_trigger(&self, name: &str) -> Option<f64> {
745 let param_id = format!("consol.{name}");
746 self.get(¶m_id).and_then(|p| p.value_f64)
747 }
748
749 pub fn consolidation_triggers(&self) -> Vec<(String, f64)> {
751 self.list(category::CONSOLIDATION_TRIGGER)
752 .into_iter()
753 .filter_map(|p| {
754 let name = p.param_id.strip_prefix("consol.")?.to_string();
755 p.value_f64.map(|v| (name, v))
756 })
757 .collect()
758 }
759}
760
761impl Default for CognitiveParameterStore {
762 fn default() -> Self {
763 Self::new()
764 }
765}
766
767fn append_row(
771 batch: &RecordBatch,
772 param: &CognitiveParameter,
773) -> Result<RecordBatch, ParamStoreError> {
774 let n = batch.num_rows();
775
776 if n == 0 {
777 return Ok(RecordBatch::try_new(
779 Arc::new(cognitive_params_schema()),
780 vec![
781 Arc::new(StringArray::from(vec![param.param_id.as_str()])),
782 Arc::new(StringArray::from(vec![param.category.as_str()])),
783 Arc::new(Float64Array::from(vec![param.value_f64])),
784 Arc::new(StringArray::from(vec![param.value_str.as_deref()])),
785 Arc::new(Float64Array::from(vec![param.min_bound])),
786 Arc::new(Float64Array::from(vec![param.max_bound])),
787 Arc::new(UInt8Array::from(vec![param.autonomy_tier as u8])),
788 Arc::new(StringArray::from(vec![param.modified_by.as_str()])),
789 ],
790 )?);
791 }
792
793 let ids = batch
795 .column(param_col::PARAM_ID)
796 .as_any()
797 .downcast_ref::<StringArray>()
798 .expect("param_id");
799 let cats = batch
800 .column(param_col::CATEGORY)
801 .as_any()
802 .downcast_ref::<StringArray>()
803 .expect("category");
804 let vals = batch
805 .column(param_col::VALUE_F64)
806 .as_any()
807 .downcast_ref::<Float64Array>()
808 .expect("value_f64");
809 let strs = batch
810 .column(param_col::VALUE_STR)
811 .as_any()
812 .downcast_ref::<StringArray>()
813 .expect("value_str");
814 let mins = batch
815 .column(param_col::MIN_BOUND)
816 .as_any()
817 .downcast_ref::<Float64Array>()
818 .expect("min_bound");
819 let maxs = batch
820 .column(param_col::MAX_BOUND)
821 .as_any()
822 .downcast_ref::<Float64Array>()
823 .expect("max_bound");
824 let tiers = batch
825 .column(param_col::AUTONOMY_TIER)
826 .as_any()
827 .downcast_ref::<UInt8Array>()
828 .expect("autonomy_tier");
829 let mods = batch
830 .column(param_col::MODIFIED_BY)
831 .as_any()
832 .downcast_ref::<StringArray>()
833 .expect("modified_by");
834
835 let mut new_ids: Vec<&str> = (0..n).map(|i| ids.value(i)).collect();
836 new_ids.push(¶m.param_id);
837
838 let mut new_cats: Vec<&str> = (0..n).map(|i| cats.value(i)).collect();
839 new_cats.push(¶m.category);
840
841 let mut new_vals: Vec<Option<f64>> = (0..n)
842 .map(|i| {
843 if vals.is_null(i) {
844 None
845 } else {
846 Some(vals.value(i))
847 }
848 })
849 .collect();
850 new_vals.push(param.value_f64);
851
852 let value_str_owned: Vec<Option<String>> = (0..n)
853 .map(|i| {
854 if strs.is_null(i) {
855 None
856 } else {
857 Some(strs.value(i).to_string())
858 }
859 })
860 .chain(std::iter::once(param.value_str.clone()))
861 .collect();
862 let new_strs: Vec<Option<&str>> = value_str_owned.iter().map(|s| s.as_deref()).collect();
863
864 let mut new_mins: Vec<Option<f64>> = (0..n)
865 .map(|i| {
866 if mins.is_null(i) {
867 None
868 } else {
869 Some(mins.value(i))
870 }
871 })
872 .collect();
873 new_mins.push(param.min_bound);
874
875 let mut new_maxs: Vec<Option<f64>> = (0..n)
876 .map(|i| {
877 if maxs.is_null(i) {
878 None
879 } else {
880 Some(maxs.value(i))
881 }
882 })
883 .collect();
884 new_maxs.push(param.max_bound);
885
886 let mut new_tiers: Vec<u8> = (0..n).map(|i| tiers.value(i)).collect();
887 new_tiers.push(param.autonomy_tier as u8);
888
889 let mut new_mods: Vec<&str> = (0..n).map(|i| mods.value(i)).collect();
890 new_mods.push(¶m.modified_by);
891
892 Ok(RecordBatch::try_new(
893 Arc::new(cognitive_params_schema()),
894 vec![
895 Arc::new(StringArray::from(new_ids)),
896 Arc::new(StringArray::from(new_cats)),
897 Arc::new(Float64Array::from(new_vals)),
898 Arc::new(StringArray::from(new_strs)),
899 Arc::new(Float64Array::from(new_mins)),
900 Arc::new(Float64Array::from(new_maxs)),
901 Arc::new(UInt8Array::from(new_tiers)),
902 Arc::new(StringArray::from(new_mods)),
903 ],
904 )?)
905}
906
907pub fn default_cognitive_params() -> CognitiveParameterStore {
918 let mut store = CognitiveParameterStore::new();
919
920 let lc = |id: &str, val: f64, min: f64, max: f64| CognitiveParameter {
922 param_id: format!("learning.{id}"),
923 category: category::LEARNING_CONFIG.to_string(),
924 value_f64: Some(val),
925 value_str: None,
926 min_bound: Some(min),
927 max_bound: Some(max),
928 autonomy_tier: AutonomyTier::Auto,
929 modified_by: "init".to_string(),
930 };
931
932 let params = vec![
933 lc("initial_learning_rate", 0.1, 0.001, 1.0),
934 lc("decayed_learning_rate", 0.01, 0.0001, 0.5),
935 lc("error_spike_learning_rate", 0.05, 0.001, 0.5),
936 lc("decay_after_decisions", 100.0, 10.0, 10000.0),
937 lc("weight_decay_lambda", 0.001, 0.0, 0.1),
938 lc("max_delta_per_step", 0.1, 0.01, 1.0),
939 lc("min_weight", -1.5, -10.0, 0.0),
940 lc("max_weight", 1.5, 0.0, 10.0),
941 lc("hebbian_lr_multiplier", 0.5, 0.0, 2.0),
942 lc("error_spike_window", 20.0, 5.0, 100.0),
943 lc("error_spike_threshold", 0.5, 0.1, 1.0),
944 ];
945
946 for p in ¶ms {
947 store
948 .insert(p)
949 .expect("default learning config insert should not fail");
950 }
951
952 let th = |id: &str, val: f64, min: f64, max: f64| CognitiveParameter {
954 param_id: format!("fusion.{id}"),
955 category: category::THRESHOLD.to_string(),
956 value_f64: Some(val),
957 value_str: None,
958 min_bound: Some(min),
959 max_bound: Some(max),
960 autonomy_tier: AutonomyTier::Auto,
961 modified_by: "init".to_string(),
962 };
963
964 let thresholds = vec![
965 th("softmax_temperature", 1.0, 0.01, 10.0),
966 th("min_signal_strength", 0.01, 0.0, 1.0),
967 th("refuse_low_coverage_threshold", 0.7, 0.0, 1.0),
968 ];
969
970 for p in &thresholds {
971 store
972 .insert(p)
973 .expect("default threshold insert should not fail");
974 }
975
976 let sm_auto = |id: &str, val: f64, min: f64, max: f64| CognitiveParameter {
979 param_id: format!("schema_match.{id}"),
980 category: category::THRESHOLD.to_string(),
981 value_f64: Some(val),
982 value_str: None,
983 min_bound: Some(min),
984 max_bound: Some(max),
985 autonomy_tier: AutonomyTier::Auto,
986 modified_by: "init".to_string(),
987 };
988 let sm_being = |id: &str, val: f64, min: f64, max: f64| CognitiveParameter {
989 param_id: format!("schema_match.{id}"),
990 category: category::THRESHOLD.to_string(),
991 value_f64: Some(val),
992 value_str: None,
993 min_bound: Some(min),
994 max_bound: Some(max),
995 autonomy_tier: AutonomyTier::BeingApproved,
996 modified_by: "init".to_string(),
997 };
998
999 let schema_thresholds = vec![
1000 sm_auto("assimilate_threshold", 0.7, 0.0, 1.0),
1001 sm_auto("accommodate_threshold", 0.3, 0.0, 1.0),
1002 sm_being("novelty_high_threshold", 0.8, 0.0, 1.0),
1003 sm_auto("assimilation_boost", 0.05, 0.0, 0.5),
1004 sm_auto("coverage_saturation", 5.0, 1.0, 50.0),
1005 ];
1006
1007 for p in &schema_thresholds {
1008 store
1009 .insert(p)
1010 .expect("default schema threshold insert should not fail");
1011 }
1012
1013 let tr = |id: &str, val: f64, min: f64, max: f64| CognitiveParameter {
1015 param_id: format!("consolidation.{id}"),
1016 category: category::TRIGGER.to_string(),
1017 value_f64: Some(val),
1018 value_str: None,
1019 min_bound: Some(min),
1020 max_bound: Some(max),
1021 autonomy_tier: AutonomyTier::BeingApproved,
1022 modified_by: "init".to_string(),
1023 };
1024
1025 let triggers = vec![
1026 tr("min_triples_for_training", 200.0, 10.0, 10000.0),
1027 tr("consolidation_cycle_interval_hours", 24.0, 1.0, 168.0),
1028 tr("kl_divergence_budget", 0.5, 0.01, 5.0),
1029 ];
1030
1031 for p in &triggers {
1032 store
1033 .insert(p)
1034 .expect("default trigger insert should not fail");
1035 }
1036
1037 let sw_store = default_signal_weights();
1039 for p in sw_store.list_all() {
1040 store
1041 .insert(&p)
1042 .expect("default signal weight insert should not fail");
1043 }
1044
1045 store
1048 .insert_loss_coefficient("alpha_lm", 1.0)
1049 .expect("default loss coefficient insert");
1050 store
1051 .insert_loss_coefficient("alpha_rel", 0.1)
1052 .expect("default loss coefficient insert");
1053 store
1054 .insert_loss_coefficient("alpha_causal", 0.05)
1055 .expect("default loss coefficient insert");
1056 store
1057 .insert_loss_coefficient("rel_warmup_fraction", 0.1)
1058 .expect("default loss coefficient insert");
1059 store
1060 .insert_loss_coefficient("causal_warmup_fraction", 0.2)
1061 .expect("default loss coefficient insert");
1062
1063 store
1067 .insert_consolidation_trigger("dedup_threshold", 0.95)
1068 .expect("default consolidation trigger insert");
1069 store
1070 .insert_consolidation_trigger("max_triples_per_cycle", 500.0)
1071 .expect("default consolidation trigger insert");
1072 store
1073 .insert_consolidation_trigger("min_promoted_triples", 50.0)
1074 .expect("default consolidation trigger insert");
1075 store
1076 .insert_consolidation_trigger("training_threshold", 200.0)
1077 .expect("default consolidation trigger insert");
1078 store
1079 .insert_consolidation_trigger("max_pairs_per_batch", 500.0)
1080 .expect("default consolidation trigger insert");
1081
1082 store
1083}
1084
1085pub fn default_signal_weights() -> CognitiveParameterStore {
1090 let mut store = CognitiveParameterStore::new();
1091
1092 let mut add = |dim: &str, path: &str, weight: f64| {
1093 store
1094 .insert_signal_weight(dim, path, weight)
1095 .expect("default signal weight insert should not fail");
1096 };
1097
1098 add("fractal_confidence", "FAST", 0.9);
1100 add("fractal_confidence", "STANDARD", 0.3);
1101 add("fractal_confidence", "DEEP", -0.5);
1102 add("fractal_confidence", "REFUSE_LOW_COVERAGE", -0.5);
1103 add("fractal_ambiguity", "FAST", -0.5);
1104 add("fractal_ambiguity", "STANDARD", 0.2);
1105 add("fractal_ambiguity", "DEEP", 0.8);
1106
1107 add("novelty_surprise", "FAST", -0.3);
1109 add("novelty_surprise", "STANDARD", 0.1);
1110 add("novelty_surprise", "DEEP", 0.7);
1111 add("novelty_surprise", "FAST_LEARNING", 0.5);
1112
1113 add("fov_temperature", "FAST", 0.4);
1115 add("fov_temperature", "STANDARD", 0.2);
1116 add("fov_temperature", "DEEP", -0.1);
1117
1118 add("emotion_confusion", "FAST", -0.2);
1120 add("emotion_confusion", "STANDARD", 0.3);
1121 add("emotion_confusion", "DEEP", 0.5);
1122
1123 add("state_importance", "FAST", -0.1);
1125 add("state_importance", "STANDARD", 0.2);
1126 add("state_importance", "DEEP", 0.5);
1127
1128 add("is_document", "TRAINING", 1.0);
1130 add("urgency", "FAST_LEARNING", 0.8);
1131
1132 add("provenance_support", "FAST", 0.8);
1134 add("provenance_support", "STANDARD", 0.2);
1135 add("provenance_support", "DEEP", -0.3);
1136 add("provenance_coverage", "FAST", 0.5);
1137 add("provenance_coverage", "STANDARD", 0.3);
1138 add("provenance_coverage", "DEEP", -0.2);
1139 add("provenance_coverage", "FAST_LEARNING", -0.4);
1140
1141 add("action_pending", "FAST", -0.2);
1143 add("action_pending", "STANDARD", 0.4);
1144 add("action_pending", "DEEP", 0.3);
1145 add("action_intent_level", "FAST", 0.6);
1146 add("action_intent_level", "STANDARD", 0.1);
1147 add("action_intent_level", "DEEP", -0.3);
1148 add("action_coverage", "FAST", 0.5);
1149 add("action_coverage", "STANDARD", 0.2);
1150 add("action_coverage", "DEEP", -0.2);
1151
1152 add("kbdd_coverage", "FAST", 0.7);
1154 add("kbdd_coverage", "STANDARD", 0.1);
1155 add("kbdd_coverage", "DEEP", -0.4);
1156 add("kbdd_coverage", "FAST_LEARNING", -0.3);
1157 add("kbdd_gap_density", "FAST", -0.5);
1158 add("kbdd_gap_density", "STANDARD", 0.2);
1159 add("kbdd_gap_density", "DEEP", 0.6);
1160 add("kbdd_gap_density", "TRAINING", 0.4);
1161 add("kbdd_gap_density", "CRYSTALLIZE", 0.6);
1162
1163 add("query_complexity", "FAST", -0.4);
1165 add("query_complexity", "STANDARD", 0.2);
1166 add("query_complexity", "DEEP", 0.6);
1167 add("query_expected_depth", "FAST", -0.3);
1168 add("query_expected_depth", "STANDARD", 0.1);
1169 add("query_expected_depth", "DEEP", 0.5);
1170 add("query_domain_match", "FAST", 0.3);
1171 add("query_domain_match", "STANDARD", 0.1);
1172 add("query_domain_match", "REFUSE_LOW_COVERAGE", -0.3);
1173
1174 add("entity_gap", "REFUSE_LOW_COVERAGE", 0.9);
1176 add("entity_gap", "CRYSTALLIZE", 0.3);
1177 add("entity_gap", "FAST", -0.4);
1178 add("entity_gap", "STANDARD", -0.2);
1179 add("coverage_gap", "REFUSE_LOW_COVERAGE", 0.7);
1180 add("coverage_gap", "CRYSTALLIZE", 0.4);
1181 add("coverage_gap", "FAST", -0.3);
1182 add("prose_available", "CRYSTALLIZE", 0.8);
1183 add("prose_available", "REFUSE_LOW_COVERAGE", -0.7);
1184 add("entity_grounding", "FAST", 0.3);
1185 add("entity_grounding", "REFUSE_LOW_COVERAGE", -0.5);
1186
1187 add("competency_match", "FAST", 0.2);
1189 add("competency_match", "STANDARD", 0.3);
1190 add("competency_match", "REFUSE_LOW_COVERAGE", -0.6);
1191 add("competency_expected_quality", "FAST", 0.3);
1192 add("competency_expected_quality", "STANDARD", 0.1);
1193 add("competency_expected_quality", "DEEP", -0.2);
1194
1195 add("pattern_applicability", "FAST", -0.3);
1197 add("pattern_applicability", "STANDARD", 0.1);
1198 add("pattern_applicability", "DEEP", 0.5);
1199 add("pattern_structured_reasoning", "FAST", -0.2);
1200 add("pattern_structured_reasoning", "DEEP", 0.4);
1201
1202 add("goal_alignment", "FAST", -0.1);
1204 add("goal_alignment", "STANDARD", 0.4);
1205 add("goal_alignment", "REFUSE_LOW_COVERAGE", -0.3);
1206 add("goal_response_priority", "STANDARD", 0.3);
1207 add("goal_response_priority", "DEEP", 0.2);
1208
1209 add("conv_turn_depth", "STANDARD", 0.2);
1211 add("conv_turn_depth", "DEEP", 0.3);
1212 add("conv_entity_continuity", "FAST", 0.3);
1213 add("conv_entity_continuity", "STANDARD", 0.1);
1214 add("conv_entity_continuity", "REFUSE_LOW_COVERAGE", -0.3);
1215 add("conv_topic_repetition", "DEEP", 0.4);
1216 add("conv_topic_repetition", "STANDARD", 0.1);
1217 add("conv_is_followup", "STANDARD", 0.3);
1218 add("conv_is_followup", "FAST", 0.1);
1219 add("conv_is_followup", "REFUSE_LOW_COVERAGE", -0.3);
1220
1221 add("growth_depth", "FAST", -0.3);
1223 add("growth_depth", "STANDARD", 0.2);
1224 add("growth_depth", "DEEP", 0.7);
1225 add("growth_richness", "FAST", -0.2);
1226 add("growth_richness", "STANDARD", 0.4);
1227 add("growth_richness", "DEEP", 0.5);
1228 add("growth_layers", "FAST", -0.1);
1229 add("growth_layers", "STANDARD", 0.5);
1230 add("growth_layers", "DEEP", 0.3);
1231 add("growth_connections", "FAST", -0.2);
1232 add("growth_connections", "STANDARD", 0.3);
1233 add("growth_connections", "DEEP", 0.5);
1234
1235 add("llm_explore_confidence", "STANDARD", 0.3);
1237 add("llm_explore_confidence", "DEEP", 0.4);
1238 add("llm_explore_confidence", "CRYSTALLIZE", 0.3);
1239 add("llm_structure_signal", "STANDARD", 0.2);
1240 add("llm_structure_signal", "DEEP", 0.5);
1241 add("llm_domain_signal", "STANDARD", 0.3);
1242 add("llm_domain_signal", "DEEP", 0.3);
1243 add("llm_relationship_richness", "STANDARD", 0.3);
1244 add("llm_relationship_richness", "DEEP", 0.4);
1245 add("llm_expansion_signal", "STANDARD", 0.2);
1246 add("llm_expansion_signal", "DEEP", 0.3);
1247 add("llm_expansion_signal", "CRYSTALLIZE", 0.2);
1248 add("llm_unknown_structure", "DEEP", 0.5);
1249 add("llm_unknown_structure", "CRYSTALLIZE", 0.4);
1250
1251 add("schema_match", "FAST", 0.5);
1253 add("schema_match", "STANDARD", 0.2);
1254 add("schema_match", "DEEP", -0.3);
1255 add("schema_match", "REFUSE_LOW_COVERAGE", -0.3);
1256 add("schema_novelty", "FAST", -0.4);
1257 add("schema_novelty", "STANDARD", 0.1);
1258 add("schema_novelty", "DEEP", 0.6);
1259 add("schema_novelty", "FAST_LEARNING", 0.4);
1260 add("schema_novelty", "TRAINING", 0.3);
1261
1262 add("tool_needed", "TOOL_USE", 0.9);
1264 add("tool_needed", "FAST", -0.2);
1265 add("tool_needed", "STANDARD", -0.1);
1266 add("tool_needed", "REFUSE_LOW_COVERAGE", -0.7);
1267 add("tool_computation", "TOOL_USE", 0.7);
1268 add("tool_computation", "DEEP", -0.3);
1269 add("tool_action", "TOOL_USE", 0.6);
1270 add("tool_match", "TOOL_USE", 0.8);
1271 add("tool_match", "REFUSE_LOW_COVERAGE", -0.5);
1272 add("tool_count", "TOOL_USE", 0.3);
1273 add("tool_auto_approve", "TOOL_USE", 0.4);
1274 add("tool_auto_approve", "FAST", 0.2);
1275 add("tool_safety_score", "TOOL_USE", 0.5);
1276 add("tool_sandbox_ok", "TOOL_USE", 0.3);
1277
1278 add("embedding_computation", "TOOL_USE", 0.8);
1280 add("embedding_computation", "FAST", -0.2);
1281 add("embedding_computation", "REFUSE_LOW_COVERAGE", -0.5);
1282 add("embedding_mean_similarity", "TOOL_USE", 0.3);
1283 add("embedding_tool_match", "TOOL_USE", 0.8);
1284 add("embedding_tool_match", "FAST", -0.2);
1285 add("embedding_tool_match", "REFUSE_LOW_COVERAGE", -0.5);
1286 add("embedding_tool_mean", "TOOL_USE", 0.3);
1287
1288 add("recency_weight", "FAST", 0.3);
1290 add("recency_weight", "STANDARD", 0.1);
1291
1292 add("wm_repetition", "FAST", 0.4);
1294 add("wm_repetition", "STANDARD", -0.1);
1295 add("wm_topic_continuity", "STANDARD", 0.3);
1296 add("wm_topic_continuity", "DEEP", 0.2);
1297
1298 add("calibration_accuracy", "FAST", 0.6);
1300 add("calibration_accuracy", "STANDARD", 0.4);
1301 add("calibration_accuracy", "REFUSE_LOW_COVERAGE", -2.5);
1302 add("calibration_confidence", "FAST", 0.3);
1303 add("calibration_confidence", "DEEP", -0.2);
1304 add("calibration_confidence", "REFUSE_LOW_COVERAGE", -0.6);
1305 add("certification_gap", "FAST", -0.5);
1306 add("certification_gap", "STANDARD", 0.2);
1307 add("certification_gap", "REFUSE_LOW_COVERAGE", 0.3);
1308 add("calibration_maturity", "FAST", 0.2);
1309 add("calibration_maturity", "REFUSE_LOW_COVERAGE", -0.3);
1310
1311 store
1312}
1313
1314pub fn save_params_to_parquet(
1321 store: &CognitiveParameterStore,
1322 path: &std::path::Path,
1323) -> Result<(), ParamStoreError> {
1324 use parquet::arrow::ArrowWriter;
1325 use std::fs;
1326
1327 if let Some(parent) = path.parent() {
1328 fs::create_dir_all(parent).map_err(|e| {
1329 ParamStoreError::InvalidSchema(format!("failed to create directory: {e}"))
1330 })?;
1331 }
1332
1333 let file = fs::File::create(path)
1334 .map_err(|e| ParamStoreError::InvalidSchema(format!("failed to create file: {e}")))?;
1335
1336 let batch = store.snapshot();
1337 let schema = Arc::new(cognitive_params_schema());
1338 let mut writer = ArrowWriter::try_new(file, schema, None)?;
1339
1340 if batch.num_rows() > 0 {
1341 writer.write(batch)?;
1342 }
1343 writer.close()?;
1344
1345 Ok(())
1346}
1347
1348pub fn load_params_from_parquet(
1352 path: &std::path::Path,
1353) -> Result<Option<CognitiveParameterStore>, ParamStoreError> {
1354 use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
1355
1356 if !path.exists() {
1357 return Ok(None);
1358 }
1359
1360 let file = std::fs::File::open(path)
1361 .map_err(|e| ParamStoreError::InvalidSchema(format!("failed to open file: {e}")))?;
1362
1363 let reader = ParquetRecordBatchReaderBuilder::try_new(file)?.build()?;
1364 let mut batches: Vec<RecordBatch> = Vec::new();
1365 for batch_result in reader {
1366 batches.push(batch_result?);
1367 }
1368
1369 if batches.is_empty() {
1370 return Ok(Some(CognitiveParameterStore::new()));
1371 }
1372
1373 if batches.len() == 1 {
1375 return Ok(Some(CognitiveParameterStore::from_batch(
1376 batches.into_iter().next().unwrap(),
1377 )?));
1378 }
1379 let schema = batches[0].schema();
1380 let combined = arrow::compute::concat_batches(&schema, &batches)?;
1381 Ok(Some(CognitiveParameterStore::from_batch(combined)?))
1382}
1383
1384#[cfg(test)]
1387mod tests {
1388 use super::*;
1389
1390 #[test]
1391 fn test_schema_has_correct_columns() {
1392 let schema = cognitive_params_schema();
1393 assert_eq!(schema.fields().len(), 8);
1394 assert_eq!(schema.field(param_col::PARAM_ID).name(), "param_id");
1395 assert_eq!(schema.field(param_col::CATEGORY).name(), "category");
1396 assert_eq!(schema.field(param_col::VALUE_F64).name(), "value_f64");
1397 assert_eq!(schema.field(param_col::VALUE_STR).name(), "value_str");
1398 assert_eq!(schema.field(param_col::MIN_BOUND).name(), "min_bound");
1399 assert_eq!(schema.field(param_col::MAX_BOUND).name(), "max_bound");
1400 assert_eq!(
1401 schema.field(param_col::AUTONOMY_TIER).name(),
1402 "autonomy_tier"
1403 );
1404 assert_eq!(schema.field(param_col::MODIFIED_BY).name(), "modified_by");
1405 }
1406
1407 #[test]
1408 fn test_empty_store() {
1409 let store = CognitiveParameterStore::new();
1410 assert!(store.is_empty());
1411 assert_eq!(store.len(), 0);
1412 assert!(store.get("anything").is_none());
1413 assert!(store.list(category::SIGNAL_WEIGHT).is_empty());
1414 }
1415
1416 #[test]
1417 fn test_insert_and_get() {
1418 let mut store = CognitiveParameterStore::new();
1419 let param = CognitiveParameter {
1420 param_id: "signal.novelty.weight".into(),
1421 category: category::SIGNAL_WEIGHT.into(),
1422 value_f64: Some(0.15),
1423 value_str: None,
1424 min_bound: Some(0.0),
1425 max_bound: Some(1.0),
1426 autonomy_tier: AutonomyTier::Auto,
1427 modified_by: "init".into(),
1428 };
1429 store.insert(¶m).unwrap();
1430
1431 assert_eq!(store.len(), 1);
1432 let got = store.get("signal.novelty.weight").unwrap();
1433 assert_eq!(got.value_f64, Some(0.15));
1434 assert_eq!(got.category, category::SIGNAL_WEIGHT);
1435 assert_eq!(got.autonomy_tier, AutonomyTier::Auto);
1436 }
1437
1438 #[test]
1439 fn test_insert_replaces_existing() {
1440 let mut store = CognitiveParameterStore::new();
1441 let p1 = CognitiveParameter {
1442 param_id: "test.param".into(),
1443 category: category::THRESHOLD.into(),
1444 value_f64: Some(1.0),
1445 value_str: None,
1446 min_bound: None,
1447 max_bound: None,
1448 autonomy_tier: AutonomyTier::Auto,
1449 modified_by: "init".into(),
1450 };
1451 store.insert(&p1).unwrap();
1452 assert_eq!(store.len(), 1);
1453
1454 let p2 = CognitiveParameter {
1455 value_f64: Some(2.0),
1456 modified_by: "hdd_loop".into(),
1457 ..p1.clone()
1458 };
1459 store.insert(&p2).unwrap();
1460 assert_eq!(store.len(), 1);
1461 assert_eq!(store.get("test.param").unwrap().value_f64, Some(2.0));
1462 assert_eq!(store.get("test.param").unwrap().modified_by, "hdd_loop");
1463 }
1464
1465 #[test]
1466 fn test_set_with_bounds_checking() {
1467 let mut store = CognitiveParameterStore::new();
1468 let param = CognitiveParameter {
1469 param_id: "bounded.param".into(),
1470 category: category::THRESHOLD.into(),
1471 value_f64: Some(0.5),
1472 value_str: None,
1473 min_bound: Some(0.0),
1474 max_bound: Some(1.0),
1475 autonomy_tier: AutonomyTier::Auto,
1476 modified_by: "init".into(),
1477 };
1478 store.insert(¶m).unwrap();
1479
1480 store
1482 .set("bounded.param", 0.8, "hdd_loop", AutonomyTier::Auto)
1483 .unwrap();
1484 assert_eq!(store.get("bounded.param").unwrap().value_f64, Some(0.8));
1485
1486 let err = store
1488 .set("bounded.param", 1.5, "hdd_loop", AutonomyTier::Auto)
1489 .unwrap_err();
1490 assert!(matches!(err, ParamStoreError::OutOfBounds { .. }));
1491
1492 let err = store
1494 .set("bounded.param", -0.1, "hdd_loop", AutonomyTier::Auto)
1495 .unwrap_err();
1496 assert!(matches!(err, ParamStoreError::OutOfBounds { .. }));
1497
1498 assert_eq!(store.get("bounded.param").unwrap().value_f64, Some(0.8));
1500 }
1501
1502 #[test]
1503 fn test_set_not_found() {
1504 let mut store = CognitiveParameterStore::new();
1505 let err = store
1506 .set("nonexistent", 1.0, "test", AutonomyTier::Auto)
1507 .unwrap_err();
1508 assert!(matches!(err, ParamStoreError::NotFound(_)));
1509 }
1510
1511 #[test]
1512 fn test_autonomy_tier_enforcement() {
1513 let mut store = CognitiveParameterStore::new();
1514 let param = CognitiveParameter {
1515 param_id: "captain.safety_rule".into(),
1516 category: category::THRESHOLD.into(),
1517 value_f64: Some(0.5),
1518 value_str: None,
1519 min_bound: Some(0.0),
1520 max_bound: Some(1.0),
1521 autonomy_tier: AutonomyTier::CaptainOnly,
1522 modified_by: "init".into(),
1523 };
1524 store.insert(¶m).unwrap();
1525
1526 let err = store
1528 .set("captain.safety_rule", 0.8, "hdd_loop", AutonomyTier::Auto)
1529 .unwrap_err();
1530 assert!(matches!(err, ParamStoreError::TierViolation { .. }));
1531
1532 let err = store
1534 .set(
1535 "captain.safety_rule",
1536 0.8,
1537 "being",
1538 AutonomyTier::BeingApproved,
1539 )
1540 .unwrap_err();
1541 assert!(matches!(err, ParamStoreError::TierViolation { .. }));
1542
1543 store
1545 .set(
1546 "captain.safety_rule",
1547 0.8,
1548 "captain",
1549 AutonomyTier::CaptainOnly,
1550 )
1551 .unwrap();
1552 assert_eq!(
1553 store.get("captain.safety_rule").unwrap().value_f64,
1554 Some(0.8)
1555 );
1556 }
1557
1558 #[test]
1559 fn test_list_by_category() {
1560 let mut store = CognitiveParameterStore::new();
1561
1562 let sw = |id: &str, val: f64| CognitiveParameter {
1563 param_id: id.into(),
1564 category: category::SIGNAL_WEIGHT.into(),
1565 value_f64: Some(val),
1566 value_str: None,
1567 min_bound: None,
1568 max_bound: None,
1569 autonomy_tier: AutonomyTier::Auto,
1570 modified_by: "init".into(),
1571 };
1572
1573 store.insert(&sw("w1", 0.1)).unwrap();
1574 store.insert(&sw("w2", 0.2)).unwrap();
1575 store
1576 .insert(&CognitiveParameter {
1577 param_id: "t1".into(),
1578 category: category::THRESHOLD.into(),
1579 value_f64: Some(0.5),
1580 value_str: None,
1581 min_bound: None,
1582 max_bound: None,
1583 autonomy_tier: AutonomyTier::Auto,
1584 modified_by: "init".into(),
1585 })
1586 .unwrap();
1587
1588 let weights = store.list(category::SIGNAL_WEIGHT);
1589 assert_eq!(weights.len(), 2);
1590
1591 let thresholds = store.list(category::THRESHOLD);
1592 assert_eq!(thresholds.len(), 1);
1593
1594 let all = store.list_all();
1595 assert_eq!(all.len(), 3);
1596 }
1597
1598 #[test]
1599 fn test_default_params_populated() {
1600 let store = default_cognitive_params();
1601 assert!(!store.is_empty());
1602
1603 let lr = store.get("learning.initial_learning_rate").unwrap();
1605 assert_eq!(lr.value_f64, Some(0.1));
1606 assert_eq!(lr.category, category::LEARNING_CONFIG);
1607 assert_eq!(lr.autonomy_tier, AutonomyTier::Auto);
1608 assert_eq!(lr.min_bound, Some(0.001));
1609 assert_eq!(lr.max_bound, Some(1.0));
1610
1611 let softmax = store.get("fusion.softmax_temperature").unwrap();
1613 assert_eq!(softmax.value_f64, Some(1.0));
1614
1615 let assim = store.get("schema_match.assimilate_threshold").unwrap();
1617 assert_eq!(assim.autonomy_tier, AutonomyTier::Auto);
1618 let novelty = store.get("schema_match.novelty_high_threshold").unwrap();
1620 assert_eq!(novelty.autonomy_tier, AutonomyTier::BeingApproved);
1621
1622 let min_triples = store.get("consolidation.min_triples_for_training").unwrap();
1624 assert_eq!(min_triples.value_f64, Some(200.0));
1625 assert_eq!(min_triples.autonomy_tier, AutonomyTier::BeingApproved);
1626 }
1627
1628 #[test]
1629 fn test_default_params_bounds_enforced() {
1630 let mut store = default_cognitive_params();
1631
1632 let err = store
1634 .set(
1635 "learning.initial_learning_rate",
1636 5.0,
1637 "test",
1638 AutonomyTier::Auto,
1639 )
1640 .unwrap_err();
1641 assert!(matches!(err, ParamStoreError::OutOfBounds { .. }));
1642
1643 store
1645 .set(
1646 "learning.initial_learning_rate",
1647 0.5,
1648 "hdd_loop",
1649 AutonomyTier::Auto,
1650 )
1651 .unwrap();
1652 assert_eq!(
1653 store
1654 .get("learning.initial_learning_rate")
1655 .unwrap()
1656 .value_f64,
1657 Some(0.5)
1658 );
1659 }
1660
1661 #[test]
1662 fn test_from_batch_validates_schema() {
1663 let bad_schema = Arc::new(Schema::new(vec![
1665 Field::new("a", DataType::Utf8, false),
1666 Field::new("b", DataType::Utf8, false),
1667 ]));
1668 let bad_batch = RecordBatch::try_new(
1669 bad_schema,
1670 vec![
1671 Arc::new(StringArray::from(vec!["x"])),
1672 Arc::new(StringArray::from(vec!["y"])),
1673 ],
1674 )
1675 .unwrap();
1676
1677 let err = CognitiveParameterStore::from_batch(bad_batch).unwrap_err();
1678 assert!(matches!(err, ParamStoreError::InvalidSchema(_)));
1679 }
1680
1681 #[test]
1682 fn test_snapshot_roundtrip() {
1683 let mut store = CognitiveParameterStore::new();
1684 let param = CognitiveParameter {
1685 param_id: "test.roundtrip".into(),
1686 category: category::THRESHOLD.into(),
1687 value_f64: Some(0.42),
1688 value_str: Some("test_value".into()),
1689 min_bound: Some(0.0),
1690 max_bound: Some(1.0),
1691 autonomy_tier: AutonomyTier::BeingApproved,
1692 modified_by: "test".into(),
1693 };
1694 store.insert(¶m).unwrap();
1695
1696 let batch = store.snapshot().clone();
1698 let store2 = CognitiveParameterStore::from_batch(batch).unwrap();
1699 let got = store2.get("test.roundtrip").unwrap();
1700 assert_eq!(got.value_f64, Some(0.42));
1701 assert_eq!(got.value_str.as_deref(), Some("test_value"));
1702 assert_eq!(got.autonomy_tier, AutonomyTier::BeingApproved);
1703 }
1704
1705 #[test]
1706 fn test_parquet_roundtrip() {
1707 let store = default_cognitive_params();
1708 let original_count = store.len();
1709
1710 let dir = tempfile::tempdir().unwrap();
1711 let path = dir.path().join("self_params.parquet");
1712
1713 save_params_to_parquet(&store, &path).unwrap();
1715 assert!(path.exists());
1716
1717 let loaded = load_params_from_parquet(&path).unwrap().unwrap();
1719 assert_eq!(loaded.len(), original_count);
1720
1721 let lr = loaded.get("learning.initial_learning_rate").unwrap();
1723 assert_eq!(lr.value_f64, Some(0.1));
1724 assert_eq!(lr.min_bound, Some(0.001));
1725 assert_eq!(lr.autonomy_tier, AutonomyTier::Auto);
1726 }
1727
1728 #[test]
1729 fn test_parquet_load_missing_file() {
1730 let result =
1731 load_params_from_parquet(std::path::Path::new("/nonexistent/params.parquet")).unwrap();
1732 assert!(result.is_none());
1733 }
1734
1735 #[test]
1736 fn test_value_str_parameter() {
1737 let mut store = CognitiveParameterStore::new();
1738 let param = CognitiveParameter {
1739 param_id: "fusion.default_path".into(),
1740 category: category::THRESHOLD.into(),
1741 value_f64: None,
1742 value_str: Some("STANDARD".into()),
1743 min_bound: None,
1744 max_bound: None,
1745 autonomy_tier: AutonomyTier::Auto,
1746 modified_by: "init".into(),
1747 };
1748 store.insert(¶m).unwrap();
1749
1750 let got = store.get("fusion.default_path").unwrap();
1751 assert!(got.value_f64.is_none());
1752 assert_eq!(got.value_str.as_deref(), Some("STANDARD"));
1753 }
1754
1755 #[test]
1756 fn test_autonomy_tier_ordering() {
1757 assert!(AutonomyTier::Auto < AutonomyTier::BeingApproved);
1758 assert!(AutonomyTier::BeingApproved < AutonomyTier::CaptainOnly);
1759 }
1760
1761 #[test]
1762 fn test_set_updates_modified_by() {
1763 let mut store = CognitiveParameterStore::new();
1764 let param = CognitiveParameter {
1765 param_id: "track.modifier".into(),
1766 category: category::THRESHOLD.into(),
1767 value_f64: Some(0.5),
1768 value_str: None,
1769 min_bound: Some(0.0),
1770 max_bound: Some(1.0),
1771 autonomy_tier: AutonomyTier::Auto,
1772 modified_by: "init".into(),
1773 };
1774 store.insert(¶m).unwrap();
1775 assert_eq!(store.get("track.modifier").unwrap().modified_by, "init");
1776
1777 store
1778 .set("track.modifier", 0.7, "hdd_loop_v2", AutonomyTier::Auto)
1779 .unwrap();
1780 assert_eq!(
1781 store.get("track.modifier").unwrap().modified_by,
1782 "hdd_loop_v2"
1783 );
1784 }
1785
1786 #[test]
1787 fn test_unbounded_param_accepts_any_value() {
1788 let mut store = CognitiveParameterStore::new();
1789 let param = CognitiveParameter {
1790 param_id: "unbounded.param".into(),
1791 category: category::THRESHOLD.into(),
1792 value_f64: Some(0.0),
1793 value_str: None,
1794 min_bound: None,
1795 max_bound: None,
1796 autonomy_tier: AutonomyTier::Auto,
1797 modified_by: "init".into(),
1798 };
1799 store.insert(¶m).unwrap();
1800
1801 store
1803 .set("unbounded.param", 999.0, "test", AutonomyTier::Auto)
1804 .unwrap();
1805 assert_eq!(store.get("unbounded.param").unwrap().value_f64, Some(999.0));
1806
1807 store
1808 .set("unbounded.param", -999.0, "test", AutonomyTier::Auto)
1809 .unwrap();
1810 assert_eq!(
1811 store.get("unbounded.param").unwrap().value_f64,
1812 Some(-999.0)
1813 );
1814 }
1815
1816 #[test]
1819 fn test_insert_signal_weight() {
1820 let mut store = CognitiveParameterStore::new();
1821 store
1822 .insert_signal_weight("fractal_confidence", "FAST", 0.9)
1823 .unwrap();
1824
1825 let val = store.get_signal_weight("fractal_confidence", "FAST");
1826 assert_eq!(val, Some(0.9));
1827
1828 let param = store.get("sw.fractal_confidence.FAST").unwrap();
1830 assert_eq!(param.category, category::SIGNAL_WEIGHT);
1831 assert_eq!(param.min_bound, Some(-1.5));
1832 assert_eq!(param.max_bound, Some(1.5));
1833 assert_eq!(param.autonomy_tier, AutonomyTier::Auto);
1834 }
1835
1836 #[test]
1837 fn test_signal_weights_list() {
1838 let mut store = CognitiveParameterStore::new();
1839 store
1840 .insert_signal_weight("fractal_confidence", "FAST", 0.9)
1841 .unwrap();
1842 store
1843 .insert_signal_weight("fractal_confidence", "DEEP", -0.5)
1844 .unwrap();
1845 store
1846 .insert_signal_weight("novelty_surprise", "STANDARD", 0.1)
1847 .unwrap();
1848
1849 let weights = store.signal_weights();
1850 assert_eq!(weights.len(), 3);
1851 assert_eq!(store.signal_weight_count(), 3);
1852
1853 let fractal_fast = weights
1855 .iter()
1856 .find(|(d, p, _)| d == "fractal_confidence" && p == "FAST");
1857 assert!(fractal_fast.is_some());
1858 assert!((fractal_fast.unwrap().2 - 0.9).abs() < 1e-10);
1859 }
1860
1861 #[test]
1862 fn test_default_signal_weights() {
1863 let store = default_signal_weights();
1864 let count = store.signal_weight_count();
1865 assert!(
1866 count >= 140,
1867 "should have >= 140 signal weight entries, got {}",
1868 count
1869 );
1870
1871 assert_eq!(
1873 store.get_signal_weight("fractal_confidence", "FAST"),
1874 Some(0.9)
1875 );
1876 assert_eq!(
1877 store.get_signal_weight("fractal_confidence", "DEEP"),
1878 Some(-0.5)
1879 );
1880 assert_eq!(
1881 store.get_signal_weight("tool_needed", "TOOL_USE"),
1882 Some(0.9)
1883 );
1884 assert_eq!(store.get_signal_weight("recency_weight", "FAST"), Some(0.3));
1885 }
1886
1887 #[test]
1888 fn test_default_params_includes_signal_weights() {
1889 let store = default_cognitive_params();
1890
1891 let sw_count = store.signal_weight_count();
1893 assert!(
1894 sw_count >= 140,
1895 "default_cognitive_params should include signal weights, got {}",
1896 sw_count
1897 );
1898
1899 let lr = store.get("learning.initial_learning_rate");
1901 assert!(lr.is_some());
1902
1903 let sw = store.get_signal_weight("fractal_confidence", "FAST");
1905 assert_eq!(sw, Some(0.9));
1906 }
1907
1908 #[test]
1909 fn test_signal_weight_bounds_enforced() {
1910 let mut store = CognitiveParameterStore::new();
1911
1912 store.insert_signal_weight("test_dim", "FAST", 2.0).unwrap();
1914 assert_eq!(
1915 store.get_signal_weight("test_dim", "FAST"),
1916 Some(1.5),
1917 "value above 1.5 should be clipped"
1918 );
1919
1920 store
1921 .insert_signal_weight("test_dim2", "FAST", -3.0)
1922 .unwrap();
1923 assert_eq!(
1924 store.get_signal_weight("test_dim2", "FAST"),
1925 Some(-1.5),
1926 "value below -1.5 should be clipped"
1927 );
1928
1929 let err = store
1931 .set("sw.test_dim.FAST", 2.0, "test", AutonomyTier::Auto)
1932 .unwrap_err();
1933 assert!(matches!(err, ParamStoreError::OutOfBounds { .. }));
1934 }
1935
1936 #[test]
1937 fn test_signal_weight_parquet_roundtrip() {
1938 let store = default_signal_weights();
1939 let original_count = store.signal_weight_count();
1940 assert!(original_count > 0);
1941
1942 let dir = tempfile::tempdir().unwrap();
1943 let path = dir.path().join("signal_weights.parquet");
1944
1945 save_params_to_parquet(&store, &path).unwrap();
1947 assert!(path.exists());
1948
1949 let loaded = load_params_from_parquet(&path).unwrap().unwrap();
1951 assert_eq!(loaded.signal_weight_count(), original_count);
1952
1953 assert_eq!(
1955 loaded.get_signal_weight("fractal_confidence", "FAST"),
1956 Some(0.9)
1957 );
1958 assert_eq!(
1959 loaded.get_signal_weight("tool_needed", "TOOL_USE"),
1960 Some(0.9)
1961 );
1962 }
1963
1964 #[test]
1967 fn test_insert_loss_coefficient() {
1968 let mut store = CognitiveParameterStore::new();
1969 store.insert_loss_coefficient("alpha_lm", 1.0).unwrap();
1970
1971 assert_eq!(store.get_loss_coefficient("alpha_lm"), Some(1.0));
1972 }
1973
1974 #[test]
1975 fn test_loss_coefficients_list() {
1976 let mut store = CognitiveParameterStore::new();
1977 store.insert_loss_coefficient("alpha_lm", 1.0).unwrap();
1978 store.insert_loss_coefficient("alpha_rel", 0.1).unwrap();
1979 store.insert_loss_coefficient("alpha_causal", 0.05).unwrap();
1980
1981 let coeffs = store.loss_coefficients();
1982 assert_eq!(coeffs.len(), 3);
1983 }
1984
1985 #[test]
1986 fn test_default_loss_coefficients_present() {
1987 let store = default_cognitive_params();
1988 assert_eq!(store.get_loss_coefficient("alpha_lm"), Some(1.0));
1989 assert_eq!(store.get_loss_coefficient("alpha_rel"), Some(0.1));
1990 assert_eq!(store.get_loss_coefficient("alpha_causal"), Some(0.05));
1991 assert_eq!(store.get_loss_coefficient("rel_warmup_fraction"), Some(0.1));
1992 assert_eq!(
1993 store.get_loss_coefficient("causal_warmup_fraction"),
1994 Some(0.2)
1995 );
1996 }
1997
1998 #[test]
1999 fn test_loss_coefficients_are_tier_auto() {
2000 let store = default_cognitive_params();
2001 let param = store.get("loss.alpha_lm").unwrap();
2002 assert_eq!(param.autonomy_tier, AutonomyTier::Auto);
2003 }
2004
2005 #[test]
2008 fn test_insert_consolidation_trigger() {
2009 let mut store = CognitiveParameterStore::new();
2010 store
2011 .insert_consolidation_trigger("dedup_threshold", 0.95)
2012 .unwrap();
2013
2014 assert_eq!(
2015 store.get_consolidation_trigger("dedup_threshold"),
2016 Some(0.95)
2017 );
2018 }
2019
2020 #[test]
2021 fn test_consolidation_triggers_list() {
2022 let mut store = CognitiveParameterStore::new();
2023 store
2024 .insert_consolidation_trigger("dedup_threshold", 0.95)
2025 .unwrap();
2026 store
2027 .insert_consolidation_trigger("max_triples_per_cycle", 500.0)
2028 .unwrap();
2029
2030 let triggers = store.consolidation_triggers();
2031 assert_eq!(triggers.len(), 2);
2032 }
2033
2034 #[test]
2035 fn test_default_consolidation_triggers_present() {
2036 let store = default_cognitive_params();
2037 assert_eq!(
2038 store.get_consolidation_trigger("dedup_threshold"),
2039 Some(0.95)
2040 );
2041 assert_eq!(
2042 store.get_consolidation_trigger("max_triples_per_cycle"),
2043 Some(500.0)
2044 );
2045 assert_eq!(
2046 store.get_consolidation_trigger("min_promoted_triples"),
2047 Some(50.0)
2048 );
2049 assert_eq!(
2050 store.get_consolidation_trigger("training_threshold"),
2051 Some(200.0)
2052 );
2053 }
2054
2055 #[test]
2056 fn test_consolidation_triggers_are_tier_being_approved() {
2057 let store = default_cognitive_params();
2058 let param = store.get("consol.dedup_threshold").unwrap();
2059 assert_eq!(param.autonomy_tier, AutonomyTier::BeingApproved);
2060 }
2061
2062 #[test]
2063 fn test_loss_and_consolidation_parquet_roundtrip() {
2064 let store = default_cognitive_params();
2065 let original_loss_count = store.loss_coefficients().len();
2066 let original_trigger_count = store.consolidation_triggers().len();
2067
2068 let dir = tempfile::tempdir().unwrap();
2069 let path = dir.path().join("params.parquet");
2070
2071 save_params_to_parquet(&store, &path).unwrap();
2072 let loaded = load_params_from_parquet(&path).unwrap().unwrap();
2073
2074 assert_eq!(loaded.loss_coefficients().len(), original_loss_count);
2075 assert_eq!(
2076 loaded.consolidation_triggers().len(),
2077 original_trigger_count
2078 );
2079 assert_eq!(loaded.get_loss_coefficient("alpha_lm"), Some(1.0));
2080 assert_eq!(
2081 loaded.get_consolidation_trigger("dedup_threshold"),
2082 Some(0.95)
2083 );
2084 }
2085}