1use std::collections::{HashMap, VecDeque};
2use std::fmt;
3use std::str::FromStr;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum SectionType {
15 Overview,
16 CoreModel,
17 BoundaryConditions,
18 Formalism,
19 OperationalGuidance,
20 Examples,
21 FailureModes,
22 ExpertLens,
23 References,
24 Other,
25}
26
27impl SectionType {
28 pub fn as_str(self) -> &'static str {
30 match self {
31 SectionType::Overview => "overview",
32 SectionType::CoreModel => "core_model",
33 SectionType::BoundaryConditions => "boundary_conditions",
34 SectionType::Formalism => "formalism",
35 SectionType::OperationalGuidance => "operational_guidance",
36 SectionType::Examples => "examples",
37 SectionType::FailureModes => "failure_modes",
38 SectionType::ExpertLens => "expert_lens",
39 SectionType::References => "references",
40 SectionType::Other => "other",
41 }
42 }
43
44 pub fn all() -> &'static [SectionType] {
46 &Self::ALL
47 }
48
49 pub const ALL: [SectionType; 10] = [
51 SectionType::Overview,
52 SectionType::CoreModel,
53 SectionType::BoundaryConditions,
54 SectionType::Formalism,
55 SectionType::OperationalGuidance,
56 SectionType::Examples,
57 SectionType::FailureModes,
58 SectionType::ExpertLens,
59 SectionType::References,
60 SectionType::Other,
61 ];
62}
63
64impl fmt::Display for SectionType {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.write_str(self.as_str())
67 }
68}
69
70impl FromStr for SectionType {
71 type Err = String;
72
73 fn from_str(s: &str) -> Result<Self, Self::Err> {
74 match s {
75 "overview" => Ok(SectionType::Overview),
76 "core_model" => Ok(SectionType::CoreModel),
77 "boundary_conditions" => Ok(SectionType::BoundaryConditions),
78 "formalism" => Ok(SectionType::Formalism),
79 "operational_guidance" => Ok(SectionType::OperationalGuidance),
80 "examples" => Ok(SectionType::Examples),
81 "failure_modes" => Ok(SectionType::FailureModes),
82 "expert_lens" => Ok(SectionType::ExpertLens),
83 "references" => Ok(SectionType::References),
84 "other" => Ok(SectionType::Other),
85 _ => Err(format!("unknown SectionType: {s:?}")),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
94pub struct BetaPosterior {
95 pub alpha: f64,
96 pub beta: f64,
97}
98
99impl BetaPosterior {
100 pub fn new(alpha: f64, beta: f64) -> Self {
101 Self { alpha, beta }
102 }
103
104 pub fn mean(&self) -> f64 {
105 self.alpha / (self.alpha + self.beta)
106 }
107
108 pub fn variance(&self) -> f64 {
109 let n = self.alpha + self.beta;
110 (self.alpha * self.beta) / (n * n * (n + 1.0))
111 }
112
113 pub fn effective_sample_size(&self) -> f64 {
114 self.alpha + self.beta
115 }
116
117 pub fn update_success(&mut self) {
118 self.alpha += 1.0;
119 }
120
121 pub fn update_failure(&mut self) {
122 self.beta += 1.0;
123 }
124
125 pub fn update_success_weighted(&mut self, weight: f64) {
130 debug_assert!(
131 weight > 0.0,
132 "update_success_weighted: weight must be positive, got {weight}"
133 );
134 self.alpha += weight;
135 }
136
137 pub fn update_failure_weighted(&mut self, weight: f64) {
142 debug_assert!(
143 weight > 0.0,
144 "update_failure_weighted: weight must be positive, got {weight}"
145 );
146 self.beta += weight;
147 }
148
149 pub fn merge(&self, other: &BetaPosterior, prior: &BetaPosterior) -> BetaPosterior {
153 BetaPosterior {
154 alpha: self.alpha + other.alpha - prior.alpha,
155 beta: self.beta + other.beta - prior.beta,
156 }
157 }
158
159 pub fn apply_ess_cap(&mut self, prior: &BetaPosterior, cap: f64) {
166 let ess = self.effective_sample_size();
167 if ess > cap {
168 let prior_ess = prior.effective_sample_size();
169 let scale = (cap - prior_ess) / (ess - prior_ess);
170 self.alpha = prior.alpha + (self.alpha - prior.alpha) * scale;
171 self.beta = prior.beta + (self.beta - prior.beta) * scale;
172 }
173 }
174
175 pub fn floored_mean(&self, floor: f64) -> f64 {
177 self.mean().max(floor)
178 }
179}
180
181impl Default for BetaPosterior {
182 fn default() -> Self {
183 Self::new(1.0, 1.0)
184 }
185}
186
187pub struct EntityPosteriors {
192 map: HashMap<Uuid, BetaPosterior>,
193 order: VecDeque<Uuid>,
194 capacity: usize,
195}
196
197impl EntityPosteriors {
198 pub fn new(capacity: usize) -> Self {
199 Self {
200 map: HashMap::with_capacity(capacity),
201 order: VecDeque::with_capacity(capacity),
202 capacity,
203 }
204 }
205
206 pub fn get_or_insert(
207 &mut self,
208 id: Uuid,
209 default: impl FnOnce() -> BetaPosterior,
210 ) -> &mut BetaPosterior {
211 if !self.map.contains_key(&id) {
212 if self.map.len() >= self.capacity {
213 if let Some(evicted) = self.order.pop_front() {
214 self.map.remove(&evicted);
215 }
216 }
217 self.map.insert(id, default());
218 self.order.push_back(id);
219 }
220 self.map.get_mut(&id).unwrap()
221 }
222
223 pub fn get(&self, id: &Uuid) -> Option<&BetaPosterior> {
224 self.map.get(id)
225 }
226
227 pub fn len(&self) -> usize {
228 self.map.len()
229 }
230
231 pub fn is_empty(&self) -> bool {
232 self.map.is_empty()
233 }
234
235 pub fn clear(&mut self) {
236 self.map.clear();
237 self.order.clear();
238 }
239
240 pub fn to_snapshot(&self) -> HashMap<Uuid, BetaPosterior> {
241 self.map.clone()
242 }
243
244 pub fn from_snapshot(snapshot: HashMap<Uuid, BetaPosterior>, capacity: usize) -> Self {
245 let mut ep = Self::new(capacity);
246 for (id, posterior) in snapshot {
247 ep.map.insert(id, posterior);
248 ep.order.push_back(id);
249 }
250 ep
251 }
252}
253
254pub struct BalancedRecallState {
261 pub relevance: BetaPosterior,
263 pub salience: BetaPosterior,
265 pub temporal: BetaPosterior,
267 pub entity_posteriors: EntityPosteriors,
269 pub total_events: u64,
271 pub exploration_epoch: u64,
273}
274
275impl BalancedRecallState {
276 pub fn new(entity_capacity: usize) -> Self {
277 Self {
278 relevance: BetaPosterior::new(7.0, 3.0),
279 salience: BetaPosterior::new(2.0, 8.0),
280 temporal: BetaPosterior::new(1.0, 9.0),
281 entity_posteriors: EntityPosteriors::new(entity_capacity),
282 total_events: 0,
283 exploration_epoch: 0,
284 }
285 }
286
287 pub fn reset_posteriors(&mut self) {
288 self.relevance = BetaPosterior::new(7.0, 3.0);
289 self.salience = BetaPosterior::new(2.0, 8.0);
290 self.temporal = BetaPosterior::new(1.0, 9.0);
291 self.entity_posteriors.clear();
292 self.exploration_epoch += 1;
293 }
294
295 pub fn to_snapshot(&self) -> BalancedRecallSnapshot {
296 BalancedRecallSnapshot {
297 relevance: self.relevance.clone(),
298 salience: self.salience.clone(),
299 temporal: self.temporal.clone(),
300 entity_posteriors: self.entity_posteriors.to_snapshot(),
301 total_events: self.total_events,
302 exploration_epoch: self.exploration_epoch,
303 }
304 }
305
306 pub fn from_snapshot(snapshot: BalancedRecallSnapshot, entity_capacity: usize) -> Self {
307 Self {
308 relevance: snapshot.relevance,
309 salience: snapshot.salience,
310 temporal: snapshot.temporal,
311 entity_posteriors: EntityPosteriors::from_snapshot(
312 snapshot.entity_posteriors,
313 entity_capacity,
314 ),
315 total_events: snapshot.total_events,
316 exploration_epoch: snapshot.exploration_epoch,
317 }
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct BalancedRecallSnapshot {
324 pub relevance: BetaPosterior,
325 pub salience: BetaPosterior,
326 pub temporal: BetaPosterior,
327 pub entity_posteriors: HashMap<Uuid, BetaPosterior>,
328 pub total_events: u64,
329 pub exploration_epoch: u64,
330}
331
332pub const DEFAULT_ESS_CAP: f64 = 100.0;
336
337pub const DEFAULT_EXPLORATION_EPOCH: u64 = 50;
339
340pub const DEFAULT_TAU_0: f64 = 1.0;
342
343pub const DEFAULT_TAU_EXPLOIT: f64 = 0.1;
345
346pub const DEFAULT_SECTION_WEIGHT_FLOOR: f64 = 0.05;
348
349pub struct SectionPosteriorState {
351 pub posteriors: HashMap<SectionType, BetaPosterior>,
352 pub priors: HashMap<SectionType, BetaPosterior>,
353 pub total_events: u64,
354 pub exploration_epoch: u64,
355}
356
357impl SectionPosteriorState {
358 pub fn new() -> Self {
360 let priors = Self::default_priors();
361 let posteriors = priors.clone();
362 Self {
363 posteriors,
364 priors,
365 total_events: 0,
366 exploration_epoch: DEFAULT_EXPLORATION_EPOCH,
367 }
368 }
369
370 pub fn from_priors(mut priors: HashMap<SectionType, BetaPosterior>) -> Self {
372 let neutral = BetaPosterior::new(2.0, 2.0);
373 for &st in SectionType::all() {
374 priors.entry(st).or_insert_with(|| neutral.clone());
375 }
376 let posteriors = priors.clone();
377 Self {
378 posteriors,
379 priors,
380 total_events: 0,
381 exploration_epoch: DEFAULT_EXPLORATION_EPOCH,
382 }
383 }
384
385 pub fn default_priors() -> HashMap<SectionType, BetaPosterior> {
387 let mut m = HashMap::new();
388 m.insert(SectionType::Overview, BetaPosterior::new(2.0, 2.0));
389 m.insert(SectionType::CoreModel, BetaPosterior::new(4.0, 2.0));
390 m.insert(
391 SectionType::BoundaryConditions,
392 BetaPosterior::new(2.0, 3.0),
393 );
394 m.insert(SectionType::Formalism, BetaPosterior::new(1.5, 4.0));
395 m.insert(
396 SectionType::OperationalGuidance,
397 BetaPosterior::new(6.0, 1.5),
398 );
399 m.insert(SectionType::Examples, BetaPosterior::new(5.0, 2.0));
400 m.insert(SectionType::FailureModes, BetaPosterior::new(3.0, 2.0));
401 m.insert(SectionType::ExpertLens, BetaPosterior::new(3.0, 2.0));
402 m.insert(SectionType::References, BetaPosterior::new(2.0, 2.0));
403 m.insert(SectionType::Other, BetaPosterior::new(2.0, 2.0));
404 m
405 }
406
407 pub fn to_snapshot(&self) -> SectionPosteriorSnapshot {
408 SectionPosteriorSnapshot {
409 posteriors: self.posteriors.clone(),
410 priors: self.priors.clone(),
411 total_events: self.total_events,
412 exploration_epoch: self.exploration_epoch,
413 }
414 }
415
416 pub fn from_snapshot(snapshot: SectionPosteriorSnapshot) -> Self {
417 Self {
418 posteriors: snapshot.posteriors,
419 priors: snapshot.priors,
420 total_events: snapshot.total_events,
421 exploration_epoch: snapshot.exploration_epoch,
422 }
423 }
424
425 pub fn weights(&self, rng: &mut impl rand::Rng) -> HashMap<SectionType, f64> {
427 if self.exploration_epoch > 0 {
428 self.sample_weights(rng)
429 } else {
430 self.deterministic_weights()
431 }
432 }
433
434 pub fn sample_weights(&self, rng: &mut impl rand::Rng) -> HashMap<SectionType, f64> {
444 let tau =
446 DEFAULT_TAU_0 * (self.exploration_epoch as f64 / DEFAULT_EXPLORATION_EPOCH as f64);
447 let tau = tau.max(1e-6); let mut samples: HashMap<SectionType, f64> = HashMap::new();
451 for (&st, posterior) in &self.posteriors {
452 let theta = sample_beta_gamma(posterior.alpha.max(1e-6), posterior.beta.max(1e-6), rng);
453 samples.insert(st, theta / tau);
454 }
455
456 let max_val = samples.values().cloned().fold(f64::NEG_INFINITY, f64::max);
458 let mut raw: HashMap<SectionType, f64> = HashMap::new();
459 for (&st, &logit) in &samples {
460 raw.insert(st, (logit - max_val).exp());
461 }
462
463 apply_floor_and_renorm(&mut raw, DEFAULT_SECTION_WEIGHT_FLOOR);
464 raw
465 }
466
467 pub fn deterministic_weights(&self) -> HashMap<SectionType, f64> {
472 let tau = DEFAULT_TAU_EXPLOIT;
473
474 let logits: HashMap<SectionType, f64> = self
476 .posteriors
477 .iter()
478 .map(|(&st, p)| (st, p.mean() / tau))
479 .collect();
480 let max_val = logits.values().cloned().fold(f64::NEG_INFINITY, f64::max);
481 let mut raw: HashMap<SectionType, f64> = HashMap::new();
482 for (&st, &logit) in &logits {
483 raw.insert(st, (logit - max_val).exp());
484 }
485
486 apply_floor_and_renorm(&mut raw, DEFAULT_SECTION_WEIGHT_FLOOR);
487 raw
488 }
489
490 pub fn reset_posteriors(&mut self) {
492 self.posteriors = self.priors.clone();
493 }
494}
495
496impl Default for SectionPosteriorState {
497 fn default() -> Self {
498 Self::new()
499 }
500}
501
502fn apply_floor_and_renorm(weights: &mut HashMap<SectionType, f64>, floor: f64) {
509 let sum: f64 = weights.values().sum();
511 if sum > 0.0 {
512 for v in weights.values_mut() {
513 *v /= sum;
514 }
515 }
516 for _ in 0..20 {
518 let (pinned_sum, n_free) = weights.values().fold((0.0f64, 0usize), |(ps, nf), &w| {
519 if w <= floor {
520 (ps + floor, nf)
521 } else {
522 (ps, nf + 1)
523 }
524 });
525 if n_free == 0 {
526 let total: f64 = weights.values().sum();
528 if total > 0.0 {
529 for v in weights.values_mut() {
530 *v /= total;
531 }
532 }
533 break;
534 }
535 let free_mass = (1.0 - pinned_sum).max(0.0);
536 let free_sum: f64 = weights.values().filter(|&&w| w > floor).sum();
537 for v in weights.values_mut() {
539 if *v <= floor {
540 *v = floor;
541 } else if free_sum > 0.0 {
542 *v = (*v / free_sum) * free_mass;
543 }
544 }
545 if weights.values().all(|&w| w >= floor - 1e-12) {
547 break;
548 }
549 }
550}
551
552fn sample_beta_gamma(alpha: f64, beta: f64, rng: &mut impl rand::Rng) -> f64 {
558 let x = sample_gamma_mt(alpha, rng);
559 let y = sample_gamma_mt(beta, rng);
560 let s = x + y;
561 if s <= 0.0 {
562 0.5
563 } else {
564 x / s
565 }
566}
567
568fn sample_gamma_mt(shape: f64, rng: &mut impl rand::Rng) -> f64 {
571 if shape < 1.0 {
572 let g = sample_gamma_mt(shape + 1.0, rng);
573 let u: f64 = rng.gen();
574 return g * u.powf(1.0 / shape);
575 }
576 let d = shape - 1.0 / 3.0;
577 let c = 1.0 / (9.0 * d).sqrt();
578 loop {
579 let x: f64 = sample_standard_normal_bm(rng);
580 let v_raw = 1.0 + c * x;
581 if v_raw <= 0.0 {
582 continue;
583 }
584 let v = v_raw * v_raw * v_raw;
585 let u: f64 = rng.gen();
586 if u < 1.0 - 0.0331 * (x * x) * (x * x) {
587 return d * v;
588 }
589 if u.ln() < 0.5 * x * x + d * (1.0 - v + v.ln()) {
590 return d * v;
591 }
592 }
593}
594
595fn sample_standard_normal_bm(rng: &mut impl rand::Rng) -> f64 {
597 let u1: f64 = rng.gen::<f64>().max(f64::EPSILON);
598 let u2: f64 = rng.gen();
599 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct SectionPosteriorSnapshot {
605 pub posteriors: HashMap<SectionType, BetaPosterior>,
606 pub priors: HashMap<SectionType, BetaPosterior>,
607 pub total_events: u64,
608 pub exploration_epoch: u64,
609}
610
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "snake_case")]
616pub enum ProfileLifecycle {
617 Defined,
619 Registered,
621 Active,
623 Inactive,
625 Archived,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct ProfileRecord {
634 pub id: String,
635 pub description: String,
636 pub consumer_kind: String,
637 pub state_class: String,
638 pub lifecycle: ProfileLifecycle,
639 pub created_at: DateTime<Utc>,
640 pub state_snapshot: Option<serde_json::Value>,
642 pub total_events: u64,
643 pub exploration_epoch: u64,
644}
645
646impl ProfileRecord {
647 pub fn new_balanced_recall(entity_capacity: usize) -> Self {
648 let state = BalancedRecallState::new(entity_capacity);
649 let snapshot = state.to_snapshot();
650 Self {
651 id: "balanced-recall-v1".into(),
652 description: "Default recall profile: three-scalar Beta posteriors (ADR-032 §5a)"
653 .into(),
654 consumer_kind: "recall".into(),
655 state_class: "Bayesian".into(),
656 lifecycle: ProfileLifecycle::Active,
657 created_at: Utc::now(),
658 state_snapshot: serde_json::to_value(snapshot).ok(),
659 total_events: 0,
660 exploration_epoch: 0,
661 }
662 }
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct ProfileBinding {
672 pub actor: String,
673 pub namespace: String,
674 pub consumer_kind: String,
675 pub profile_id: String,
676 pub priority: i32,
677 pub created_at: DateTime<Utc>,
678}
679
680pub struct BrainState {
696 pub profiles: HashMap<String, ProfileRecord>,
698 pub balanced_recall: BalancedRecallState,
700 pub profile_states: HashMap<String, BalancedRecallState>,
702 pub bindings: Vec<ProfileBinding>,
704 pub section_states: HashMap<String, SectionPosteriorState>,
706}
707
708impl BrainState {
709 pub fn new(entity_capacity: usize) -> Self {
710 let mut profiles = HashMap::new();
711 let record = ProfileRecord::new_balanced_recall(entity_capacity);
712 let profile_id = record.id.clone();
713 profiles.insert(profile_id.clone(), record);
714
715 Self {
716 profiles,
717 balanced_recall: BalancedRecallState::new(entity_capacity),
718 profile_states: HashMap::new(),
719 bindings: Vec::new(),
720 section_states: HashMap::new(),
721 }
722 }
723
724 pub fn to_snapshot(&self) -> BrainStateSnapshot {
725 let extra: HashMap<String, BalancedRecallSnapshot> = self
726 .profile_states
727 .iter()
728 .map(|(id, s)| (id.clone(), s.to_snapshot()))
729 .collect();
730 let section_states: HashMap<String, SectionPosteriorSnapshot> = self
731 .section_states
732 .iter()
733 .map(|(id, s)| (id.clone(), s.to_snapshot()))
734 .collect();
735 BrainStateSnapshot {
736 profiles: self.profiles.clone(),
737 balanced_recall: self.balanced_recall.to_snapshot(),
738 profile_states: extra,
739 bindings: self.bindings.clone(),
740 section_states,
741 }
742 }
743
744 pub fn from_snapshot(snapshot: BrainStateSnapshot, entity_capacity: usize) -> Self {
745 let extra: HashMap<String, BalancedRecallState> = snapshot
746 .profile_states
747 .into_iter()
748 .map(|(id, s)| (id, BalancedRecallState::from_snapshot(s, entity_capacity)))
749 .collect();
750 let section_states: HashMap<String, SectionPosteriorState> = snapshot
751 .section_states
752 .into_iter()
753 .map(|(id, s)| (id, SectionPosteriorState::from_snapshot(s)))
754 .collect();
755 Self {
756 profiles: snapshot.profiles,
757 balanced_recall: BalancedRecallState::from_snapshot(
758 snapshot.balanced_recall,
759 entity_capacity,
760 ),
761 profile_states: extra,
762 bindings: snapshot.bindings,
763 section_states,
764 }
765 }
766
767 pub fn reset_posteriors(&mut self) {
769 self.balanced_recall.reset_posteriors();
770 if let Some(record) = self.profiles.get_mut("balanced-recall-v1") {
771 record.exploration_epoch = self.balanced_recall.exploration_epoch;
772 record.state_snapshot = serde_json::to_value(self.balanced_recall.to_snapshot()).ok();
773 }
774 if let Some(ss) = self.section_states.get_mut("balanced-recall-v1") {
775 ss.reset_posteriors();
776 }
777 }
778
779 pub fn reset_profile_posteriors(&mut self, profile_id: &str) {
781 if let Some(ps) = self.profile_states.get_mut(profile_id) {
782 ps.reset_posteriors();
783 let snap = serde_json::to_value(ps.to_snapshot()).ok();
784 let epoch = ps.exploration_epoch;
785 if let Some(record) = self.profiles.get_mut(profile_id) {
786 record.exploration_epoch = epoch;
787 record.state_snapshot = snap;
788 }
789 }
790 if let Some(ss) = self.section_states.get_mut(profile_id) {
791 ss.reset_posteriors();
792 }
793 }
794
795 pub fn resolve(
803 &self,
804 actor: Option<&str>,
805 namespace: Option<&str>,
806 consumer_kind: &str,
807 ) -> Option<&ProfileRecord> {
808 self.resolve_with_match(actor, namespace, consumer_kind)
809 .map(|(record, _)| record)
810 }
811
812 pub fn resolve_with_match(
819 &self,
820 actor: Option<&str>,
821 namespace: Option<&str>,
822 consumer_kind: &str,
823 ) -> Option<(&ProfileRecord, String)> {
824 let actor_val = actor.unwrap_or("*");
825 let namespace_val = namespace.unwrap_or("*");
826
827 let best = self
831 .bindings
832 .iter()
833 .filter(|b| {
834 (b.actor == "*" || b.actor == actor_val)
835 && (b.namespace == "*" || b.namespace == namespace_val)
836 && (b.consumer_kind == "*" || b.consumer_kind == consumer_kind)
837 && self
838 .profiles
839 .get(&b.profile_id)
840 .is_some_and(|p| p.lifecycle != ProfileLifecycle::Archived)
841 })
842 .max_by_key(|b| {
843 let actor_score = if b.actor != "*" { 4 } else { 0 };
844 let ns_score = if b.namespace != "*" { 2 } else { 0 };
845 let kind_score = if b.consumer_kind != "*" { 1 } else { 0 };
846 (
847 actor_score + ns_score + kind_score,
848 b.priority,
849 -(b.created_at.timestamp()),
850 )
851 });
852
853 if let Some(binding) = best {
854 if let Some(record) = self.profiles.get(&binding.profile_id) {
855 return Some((record, binding.consumer_kind.clone()));
856 }
857 }
859
860 if let Some(default) = self.profiles.get("balanced-recall-v1") {
864 if default.lifecycle == ProfileLifecycle::Active
865 && (default.consumer_kind == consumer_kind
866 || consumer_kind == "*"
867 || default.consumer_kind == "*")
868 {
869 return Some((default, default.consumer_kind.clone()));
870 }
871 }
872
873 self.profiles.values().find_map(|p| {
875 if p.consumer_kind == consumer_kind && p.lifecycle == ProfileLifecycle::Active {
876 Some((p, p.consumer_kind.clone()))
877 } else {
878 None
879 }
880 })
881 }
882}
883
884#[derive(Debug, Clone, Serialize, Deserialize)]
886pub struct BrainStateSnapshot {
887 pub profiles: HashMap<String, ProfileRecord>,
888 pub balanced_recall: BalancedRecallSnapshot,
889 #[serde(default)]
891 pub profile_states: HashMap<String, BalancedRecallSnapshot>,
892 pub bindings: Vec<ProfileBinding>,
893 #[serde(default)]
895 pub section_states: HashMap<String, SectionPosteriorSnapshot>,
896}
897
898#[cfg(test)]
899mod tests {
900 use super::*;
901
902 #[test]
903 fn beta_posterior_mean() {
904 let p = BetaPosterior::new(7.0, 3.0);
905 assert!((p.mean() - 0.7).abs() < 1e-12);
906 }
907
908 #[test]
909 fn beta_posterior_variance() {
910 let p = BetaPosterior::new(7.0, 3.0);
911 let expected = 21.0 / 1100.0;
912 assert!((p.variance() - expected).abs() < 1e-12);
913 }
914
915 #[test]
916 fn beta_posterior_ess() {
917 let p = BetaPosterior::new(7.0, 3.0);
918 assert!((p.effective_sample_size() - 10.0).abs() < 1e-12);
919 }
920
921 #[test]
922 fn beta_posterior_update() {
923 let mut p = BetaPosterior::new(1.0, 1.0);
924 p.update_success();
925 p.update_success();
926 p.update_failure();
927 assert!((p.alpha - 3.0).abs() < 1e-12);
928 assert!((p.beta - 2.0).abs() < 1e-12);
929 assert!((p.mean() - 0.6).abs() < 1e-12);
930 }
931
932 #[test]
933 fn beta_posterior_merge() {
934 let prior = BetaPosterior::new(2.0, 8.0);
935 let a = BetaPosterior::new(5.0, 9.0); let b = BetaPosterior::new(4.0, 10.0); let merged = a.merge(&b, &prior);
938 assert!((merged.alpha - 7.0).abs() < 1e-12);
940 assert!((merged.beta - 11.0).abs() < 1e-12);
941 }
942
943 #[test]
944 fn beta_posterior_apply_ess_cap_noop() {
945 let prior = BetaPosterior::new(2.0, 2.0);
947 let mut p = BetaPosterior::new(7.0, 3.0);
948 p.apply_ess_cap(&prior, 50.0);
949 assert!((p.alpha - 7.0).abs() < 1e-12);
950 assert!((p.beta - 3.0).abs() < 1e-12);
951 }
952
953 #[test]
954 fn beta_posterior_apply_ess_cap_rescale() {
955 let prior = BetaPosterior::new(2.0, 2.0);
960 let mut p = BetaPosterior::new(30.0, 30.0);
961 p.apply_ess_cap(&prior, 50.0);
962 let scale = (50.0 - 4.0) / (60.0 - 4.0); let expected_excess = 28.0 * scale;
964 assert!((p.alpha - (2.0 + expected_excess)).abs() < 1e-10);
965 assert!((p.beta - (2.0 + expected_excess)).abs() < 1e-10);
966 assert!((p.effective_sample_size() - 50.0).abs() < 1e-10);
967 }
968
969 #[test]
970 fn beta_posterior_floored_mean() {
971 let p = BetaPosterior::new(1.0, 99.0); assert!((p.floored_mean(0.05) - 0.05).abs() < 1e-12);
973
974 let p2 = BetaPosterior::new(7.0, 3.0); assert!((p2.floored_mean(0.05) - 0.7).abs() < 1e-12);
976 }
977
978 #[test]
979 fn entity_posteriors_eviction() {
980 let mut ep = EntityPosteriors::new(3);
981 let ids: Vec<Uuid> = (0..5).map(|_| Uuid::new_v4()).collect();
982 for id in &ids {
983 ep.get_or_insert(*id, BetaPosterior::default);
984 }
985 assert_eq!(ep.len(), 3);
986 assert!(ep.get(&ids[0]).is_none());
987 assert!(ep.get(&ids[1]).is_none());
988 assert!(ep.get(&ids[2]).is_some());
989 assert!(ep.get(&ids[3]).is_some());
990 assert!(ep.get(&ids[4]).is_some());
991 }
992
993 #[test]
994 fn entity_posteriors_get_or_insert_existing() {
995 let mut ep = EntityPosteriors::new(10);
996 let id = Uuid::new_v4();
997 ep.get_or_insert(id, BetaPosterior::default)
998 .update_success();
999 let p = ep.get_or_insert(id, BetaPosterior::default);
1000 assert!((p.alpha - 2.0).abs() < 1e-12);
1001 }
1002
1003 #[test]
1004 fn balanced_recall_state_snapshot_roundtrip() {
1005 let mut state = BalancedRecallState::new(100);
1006 state.relevance.update_success();
1007 state.total_events = 42;
1008 let id = Uuid::new_v4();
1009 state
1010 .entity_posteriors
1011 .get_or_insert(id, BetaPosterior::default)
1012 .update_success();
1013
1014 let snapshot = state.to_snapshot();
1015 let json = serde_json::to_string(&snapshot).unwrap();
1016 let back: BalancedRecallSnapshot = serde_json::from_str(&json).unwrap();
1017 assert_eq!(back.total_events, 42);
1018 assert!((back.relevance.alpha - 8.0).abs() < 1e-12);
1019 assert!(back.entity_posteriors.contains_key(&id));
1020 }
1021
1022 #[test]
1023 fn balanced_recall_state_reset_preserves_epoch_increment() {
1024 let mut state = BalancedRecallState::new(10);
1025 state.total_events = 100;
1026 state.reset_posteriors();
1027 assert_eq!(state.total_events, 100);
1028 assert_eq!(state.exploration_epoch, 1);
1029 assert!((state.relevance.alpha - 7.0).abs() < 1e-12);
1030 assert!((state.relevance.beta - 3.0).abs() < 1e-12);
1031 }
1032
1033 #[test]
1034 fn brain_state_has_balanced_recall_profile_by_default() {
1035 let state = BrainState::new(100);
1036 assert!(state.profiles.contains_key("balanced-recall-v1"));
1037 let record = &state.profiles["balanced-recall-v1"];
1038 assert_eq!(record.lifecycle, ProfileLifecycle::Active);
1039 assert_eq!(record.consumer_kind, "recall");
1040 assert_eq!(record.state_class, "Bayesian");
1041 }
1042
1043 #[test]
1044 fn brain_state_reset_posteriors_updates_record() {
1045 let mut state = BrainState::new(10);
1046 state.balanced_recall.relevance.update_success();
1047 state.balanced_recall.total_events = 50;
1048 state.reset_posteriors();
1049 assert_eq!(state.balanced_recall.exploration_epoch, 1);
1050 let record = &state.profiles["balanced-recall-v1"];
1051 assert_eq!(record.exploration_epoch, 1);
1052 }
1053
1054 #[test]
1055 fn brain_state_resolve_falls_back_to_default() {
1056 let state = BrainState::new(100);
1057 let resolved = state.resolve(None, None, "recall");
1058 assert!(resolved.is_some());
1059 assert_eq!(resolved.unwrap().id, "balanced-recall-v1");
1060 }
1061
1062 #[test]
1063 fn brain_state_resolve_uses_explicit_binding() {
1064 let mut state = BrainState::new(100);
1065 let mut alt = ProfileRecord::new_balanced_recall(100);
1067 alt.id = "alt-profile".into();
1068 state.profiles.insert("alt-profile".into(), alt);
1069
1070 state.bindings.push(ProfileBinding {
1072 actor: "agent-1".into(),
1073 namespace: "*".into(),
1074 consumer_kind: "recall".into(),
1075 profile_id: "alt-profile".into(),
1076 priority: 0,
1077 created_at: Utc::now(),
1078 });
1079
1080 let resolved = state.resolve(Some("agent-1"), None, "recall");
1081 assert!(resolved.is_some());
1082 assert_eq!(resolved.unwrap().id, "alt-profile");
1083
1084 let resolved_other = state.resolve(Some("agent-2"), None, "recall");
1086 assert_eq!(resolved_other.unwrap().id, "balanced-recall-v1");
1087 }
1088
1089 #[test]
1092 fn brain_state_resolve_skips_archived_default() {
1093 let mut state = BrainState::new(100);
1094
1095 state
1097 .profiles
1098 .get_mut("balanced-recall-v1")
1099 .expect("default profile always exists")
1100 .lifecycle = ProfileLifecycle::Archived;
1101
1102 let resolved = state.resolve(None, None, "recall");
1104 assert!(
1105 resolved.is_none(),
1106 "archived default profile must not be returned by resolve"
1107 );
1108 }
1109
1110 #[test]
1111 fn entity_posteriors_from_snapshot_rebuilds_map() {
1112 let id1 = Uuid::new_v4();
1113 let id2 = Uuid::new_v4();
1114 let mut snapshot = HashMap::new();
1115 snapshot.insert(id1, BetaPosterior::new(3.0, 2.0));
1116 snapshot.insert(id2, BetaPosterior::new(5.0, 1.0));
1117
1118 let ep = EntityPosteriors::from_snapshot(snapshot, 100);
1119 assert_eq!(ep.len(), 2);
1120 let p1 = ep.get(&id1).unwrap();
1121 assert!((p1.alpha - 3.0).abs() < 1e-12);
1122 let p2 = ep.get(&id2).unwrap();
1123 assert!((p2.alpha - 5.0).abs() < 1e-12);
1124 }
1125
1126 #[test]
1127 fn brain_state_snapshot_roundtrip() {
1128 let mut state = BrainState::new(100);
1129 state.balanced_recall.relevance.update_success();
1130 state.balanced_recall.total_events = 55;
1131 state.balanced_recall.exploration_epoch = 2;
1132 let id = Uuid::new_v4();
1133 state
1134 .balanced_recall
1135 .entity_posteriors
1136 .get_or_insert(id, || BetaPosterior::new(4.0, 6.0))
1137 .update_success();
1138
1139 let snap1 = state.to_snapshot();
1140 let restored = BrainState::from_snapshot(snap1, 100);
1141 let snap2 = restored.to_snapshot();
1142
1143 assert_eq!(snap2.balanced_recall.total_events, 55);
1144 assert_eq!(snap2.balanced_recall.exploration_epoch, 2);
1145 assert!((snap2.balanced_recall.relevance.alpha - 8.0).abs() < 1e-12);
1146 let ep = snap2.balanced_recall.entity_posteriors.get(&id).unwrap();
1147 assert!((ep.alpha - 5.0).abs() < 1e-12);
1148 assert!((ep.beta - 6.0).abs() < 1e-12);
1149 }
1150
1151 #[test]
1152 fn profile_lifecycle_serde_roundtrip() {
1153 let lc = ProfileLifecycle::Active;
1154 let json = serde_json::to_string(&lc).unwrap();
1155 let back: ProfileLifecycle = serde_json::from_str(&json).unwrap();
1156 assert_eq!(back, ProfileLifecycle::Active);
1157 }
1158
1159 #[test]
1160 fn beta_posterior_default_has_uniform_prior() {
1161 let p = BetaPosterior::default();
1162 assert!((p.alpha - 1.0).abs() < 1e-12);
1163 assert!((p.beta - 1.0).abs() < 1e-12);
1164 assert!((p.mean() - 0.5).abs() < 1e-12);
1165 }
1166
1167 #[test]
1168 fn section_type_serde_roundtrip() {
1169 for &st in SectionType::all() {
1170 let json = serde_json::to_string(&st).unwrap();
1171 let back: SectionType = serde_json::from_str(&json).unwrap();
1172 assert_eq!(back, st, "roundtrip failed for {st}");
1173 }
1174 }
1175
1176 #[test]
1177 fn section_type_display_and_from_str() {
1178 for &st in SectionType::all() {
1179 let s = st.to_string();
1180 let parsed: SectionType = s.parse().expect("parse should succeed");
1181 assert_eq!(parsed, st);
1182 }
1183 }
1184
1185 #[test]
1186 fn section_type_from_str_unknown_rejected() {
1187 let result = "unknown_section".parse::<SectionType>();
1188 assert!(result.is_err());
1189 }
1190
1191 #[test]
1192 fn section_posterior_state_new_has_all_sections() {
1193 let state = SectionPosteriorState::new();
1194 assert_eq!(state.posteriors.len(), 10);
1195 assert_eq!(state.priors.len(), 10);
1196 for &st in SectionType::all() {
1197 assert!(
1198 state.posteriors.contains_key(&st),
1199 "missing section {st} in posteriors"
1200 );
1201 assert!(
1202 state.priors.contains_key(&st),
1203 "missing section {st} in priors"
1204 );
1205 }
1206 }
1207
1208 #[test]
1209 fn section_posterior_state_snapshot_roundtrip() {
1210 let state = SectionPosteriorState::new();
1211 let snap = state.to_snapshot();
1212 let json = serde_json::to_string(&snap).unwrap();
1213 let back: SectionPosteriorSnapshot = serde_json::from_str(&json).unwrap();
1214 assert_eq!(back.posteriors.len(), 10);
1215 assert_eq!(back.total_events, 0);
1216 assert_eq!(back.exploration_epoch, DEFAULT_EXPLORATION_EPOCH);
1217
1218 let restored = SectionPosteriorState::from_snapshot(back);
1220 assert_eq!(restored.posteriors.len(), 10);
1221 let op = &state.posteriors[&SectionType::OperationalGuidance];
1222 let rp = &restored.posteriors[&SectionType::OperationalGuidance];
1223 assert!((op.alpha - rp.alpha).abs() < 1e-12);
1224 assert!((op.beta - rp.beta).abs() < 1e-12);
1225 }
1226
1227 #[test]
1228 fn section_posterior_state_deterministic_weights_normalized() {
1229 let state = SectionPosteriorState::new();
1230 let weights = state.deterministic_weights();
1231 assert_eq!(weights.len(), 10);
1232 let sum: f64 = weights.values().sum();
1233 assert!((sum - 1.0).abs() < 1e-10, "weights sum = {sum}");
1234 for (&st, &w) in &weights {
1235 assert!(
1236 w >= DEFAULT_SECTION_WEIGHT_FLOOR - 1e-12,
1237 "weight for {st} below floor: {w}"
1238 );
1239 }
1240 }
1241
1242 #[test]
1243 fn section_posterior_state_from_priors_fills_missing() {
1244 let mut partial: HashMap<SectionType, BetaPosterior> = HashMap::new();
1246 partial.insert(SectionType::Overview, BetaPosterior::new(5.0, 1.0));
1247 partial.insert(SectionType::CoreModel, BetaPosterior::new(4.0, 2.0));
1248 partial.insert(SectionType::Formalism, BetaPosterior::new(3.0, 3.0));
1249
1250 let state = SectionPosteriorState::from_priors(partial);
1251 assert_eq!(state.priors.len(), 10);
1252 assert_eq!(state.posteriors.len(), 10);
1253
1254 assert!((state.priors[&SectionType::Overview].alpha - 5.0).abs() < 1e-12);
1256
1257 let neutral = &state.priors[&SectionType::Examples];
1259 assert!((neutral.alpha - 2.0).abs() < 1e-12);
1260 assert!((neutral.beta - 2.0).abs() < 1e-12);
1261 }
1262
1263 #[test]
1264 fn beta_posterior_weighted_success_adds_weight_to_alpha() {
1265 let mut p = BetaPosterior::new(2.0, 8.0);
1266 p.update_success_weighted(1.5);
1267 assert!((p.alpha - 3.5).abs() < 1e-12);
1268 assert!((p.beta - 8.0).abs() < 1e-12);
1269 }
1270
1271 #[test]
1272 fn beta_posterior_weighted_failure_adds_weight_to_beta() {
1273 let mut p = BetaPosterior::new(2.0, 8.0);
1274 p.update_failure_weighted(2.0);
1275 assert!((p.alpha - 2.0).abs() < 1e-12);
1276 assert!((p.beta - 10.0).abs() < 1e-12);
1277 }
1278
1279 #[test]
1280 fn beta_posterior_weighted_fractional_update() {
1281 let mut p = BetaPosterior::new(1.0, 1.0);
1282 p.update_success_weighted(0.5);
1283 assert!((p.alpha - 1.5).abs() < 1e-12);
1284 assert!((p.beta - 1.0).abs() < 1e-12);
1285 }
1286
1287 #[cfg(debug_assertions)]
1288 #[test]
1289 #[should_panic(expected = "update_success_weighted: weight must be positive")]
1290 fn beta_posterior_weighted_success_rejects_zero_weight() {
1291 let mut p = BetaPosterior::new(1.0, 1.0);
1292 p.update_success_weighted(0.0);
1293 }
1294
1295 #[cfg(debug_assertions)]
1296 #[test]
1297 #[should_panic(expected = "update_failure_weighted: weight must be positive")]
1298 fn beta_posterior_weighted_failure_rejects_negative_weight() {
1299 let mut p = BetaPosterior::new(1.0, 1.0);
1300 p.update_failure_weighted(-1.0);
1301 }
1302}