Skip to main content

khive_pack_brain/
lib.rs

1//! pack-brain — profile management registry for khive.
2
3pub mod fold;
4pub mod fold_gate;
5pub mod handlers;
6pub mod persist;
7pub mod serve_ledger;
8pub mod tunable;
9
10mod event;
11mod pack;
12
13pub(crate) use pack::sync_balanced_recall_record;
14pub use pack::{BrainPack, ENTITY_CACHE_CAPACITY};
15
16use std::collections::HashMap;
17
18use khive_brain_core::{SectionPosteriorState, SectionType};
19
20/// Ensure `profile_id` has a fully-seeded `SectionPosteriorState` in `section_states`.
21///
22/// Gets-or-inserts the profile entry via `entry(..).or_default()`, then backfills any
23/// missing `posteriors`/`priors` slots from `SectionPosteriorState::default_priors()`
24/// across all `SectionType::all()` variants.
25///
26/// Used by both the live `brain.feedback` handler and the persisted event-replay path
27/// so that section signals are applied identically regardless of whether the loaded
28/// snapshot predates section_states or a new `SectionType` variant was added after it.
29pub(crate) fn ensure_section_state_seeded<'a>(
30    section_states: &'a mut HashMap<String, SectionPosteriorState>,
31    profile_id: &str,
32) -> &'a mut SectionPosteriorState {
33    let section_state = section_states.entry(profile_id.to_owned()).or_default();
34    let defaults = SectionPosteriorState::default_priors();
35    for st in SectionType::all() {
36        if let Some(prior) = defaults.get(st) {
37            section_state
38                .posteriors
39                .entry(*st)
40                .or_insert_with(|| prior.clone());
41            section_state
42                .priors
43                .entry(*st)
44                .or_insert_with(|| prior.clone());
45        }
46    }
47    section_state
48}
49
50/// Validate a `section_signals` JSON value before any state mutation.
51///
52/// Enforces the section fold contract (ADR-048): keys must be known `SectionType`
53/// names; values must be `useful`, `not_useful`, or `wrong`; and the map must not
54/// be empty (an empty map carries no evidence and must not advance posterior state).
55///
56/// Used by both `brain.feedback` live handler and replay to ensure a single,
57/// consistent contract.
58pub(crate) fn validate_section_signals(
59    ss: &serde_json::Value,
60) -> Result<(), khive_runtime::RuntimeError> {
61    let obj = ss.as_object().ok_or_else(|| {
62        khive_runtime::RuntimeError::InvalidInput(
63            "section_signals must be a JSON object mapping section names to signal strings".into(),
64        )
65    })?;
66    if obj.is_empty() {
67        return Err(khive_runtime::RuntimeError::InvalidInput(
68            "section_signals must not be empty; omit the field entirely to submit feedback \
69             without section evidence"
70                .into(),
71        ));
72    }
73    // Section fold (ADR-048) only handles useful | not_useful | wrong.
74    // Semantic event kinds (explicit_positive, correction, …) belong to the profile-level
75    // signal and are not valid per-section values.
76    let valid_signals = ["useful", "not_useful", "wrong"];
77    let valid_sections = khive_brain_core::SectionType::NAMES;
78    for (key, val) in obj {
79        if !valid_sections.contains(&key.as_str()) {
80            return Err(khive_runtime::RuntimeError::InvalidInput(format!(
81                "section_signals: unknown section {key:?}; valid: {}",
82                valid_sections.join(", ")
83            )));
84        }
85        let sig = val.as_str().ok_or_else(|| {
86            khive_runtime::RuntimeError::InvalidInput(format!(
87                "section_signals: signal for section {key:?} must be one of: useful | not_useful | wrong"
88            ))
89        })?;
90        if !valid_signals.contains(&sig) {
91            return Err(khive_runtime::RuntimeError::InvalidInput(format!(
92                "section_signals: invalid signal {sig:?} for section {key:?}; \
93                 valid: {}",
94                valid_signals.join(" | ")
95            )));
96        }
97    }
98    Ok(())
99}
100
101#[cfg(test)]
102mod tests;