1pub mod event;
2pub mod fold;
3pub mod persist;
4pub mod section;
5pub mod state;
6pub mod tunable;
7
8use std::{sync::Mutex, time::Instant};
9
10use async_trait::async_trait;
11use chrono::Utc;
12use serde::Deserialize;
13use serde_json::{json, Value};
14
15use khive_fold::{Fold, FoldContext};
16use khive_runtime::pack::PackRuntime;
17use khive_runtime::{
18 micros_to_iso, DispatchHook, EventView, KhiveRuntime, NamespaceToken, RuntimeError,
19 VerbRegistry,
20};
21use khive_storage::event::{Event, EventFilter};
22use khive_storage::types::PageRequest;
23use khive_types::{HandlerDef, Pack, ParamDef, VerbCategory, Visibility};
24
25use crate::fold::{BalancedRecallFold, SectionPosteriorFold};
26use crate::section::derive_deterministic_weights;
27use crate::state::{
28 BrainState, ProfileBinding, ProfileLifecycle, ProfileRecord, SectionPosteriorState, SectionType,
29};
30
31const ENTITY_CACHE_CAPACITY: usize = 10_000;
32
33pub(crate) fn sync_balanced_recall_record(state: &mut BrainState) {
43 let total_ev = state.balanced_recall.total_events;
44 let snap_val = serde_json::to_value(state.balanced_recall.to_snapshot()).ok();
45 if let Some(record) = state.profiles.get_mut("balanced-recall-v1") {
46 record.total_events = total_ev;
47 record.state_snapshot = snap_val;
48 }
49}
50
51static BRAIN_HANDLERS: &[HandlerDef] = &[
60 HandlerDef {
62 name: "brain.state",
63 description: "Return current BrainState snapshot for inspection",
64 visibility: Visibility::Subhandler,
65 category: VerbCategory::Assertive,
66 params: &[],
67 },
68 HandlerDef {
69 name: "brain.config",
70 description: "Return projected config for a named pack parameter",
71 visibility: Visibility::Subhandler,
72 category: VerbCategory::Assertive,
73 params: &[ParamDef {
74 name: "parameter",
75 param_type: "string",
76 required: false,
77 description: "Specific parameter to query: \"recall::relevance_weight\" | \"recall::salience_weight\" | \"recall::temporal_weight\". Omit to return all.",
78 }],
79 },
80 HandlerDef {
81 name: "brain.events",
82 description: "List recent brain-relevant events for debugging",
83 visibility: Visibility::Subhandler,
84 category: VerbCategory::Assertive,
85 params: &[ParamDef {
86 name: "limit",
87 param_type: "integer",
88 required: false,
89 description: "Maximum events to return (default 20, max 100).",
90 }],
91 },
92 HandlerDef {
93 name: "brain.profiles",
94 description: "List profiles, optionally filtered by lifecycle",
95 visibility: Visibility::Verb,
96 category: VerbCategory::Assertive,
97 params: &[ParamDef {
98 name: "lifecycle",
99 param_type: "string",
100 required: false,
101 description: "Filter profiles by lifecycle state: \"active\" | \"inactive\" | \"archived\". Omit to return all.",
102 }],
103 },
104 HandlerDef {
105 name: "brain.profile",
106 description: "Profile metadata, latest snapshot, current state summary",
107 visibility: Visibility::Verb,
108 category: VerbCategory::Assertive,
109 params: &[ParamDef {
110 name: "profile_id",
111 param_type: "string",
112 required: true,
113 description: "Profile ID string (e.g. \"balanced-recall-v1\"). NOT a UUID — use the string identifier. Alias: id.",
114 }],
115 },
116 HandlerDef {
117 name: "brain.resolve",
118 description: "Show which profile would serve a caller context",
119 visibility: Visibility::Verb,
120 category: VerbCategory::Assertive,
121 params: &[
122 ParamDef {
123 name: "consumer_kind",
124 param_type: "string",
125 required: true,
126 description: "Verb or operation type the caller is about to perform (e.g. \"recall\").",
127 },
128 ParamDef {
129 name: "actor",
130 param_type: "string",
131 required: false,
132 description: "Caller actor identifier. Defaults to \"*\" wildcard match.",
133 },
134 ParamDef {
135 name: "namespace",
136 param_type: "string",
137 required: false,
138 description: "Namespace for binding resolution. Defaults to \"*\" wildcard match.",
139 },
140 ],
141 },
142 HandlerDef {
144 name: "brain.activate",
145 description: "Move a profile to Active (start live update loop)",
146 visibility: Visibility::Verb,
147 category: VerbCategory::Commissive,
148 params: &[ParamDef {
149 name: "profile_id",
150 param_type: "string",
151 required: true,
152 description: "Profile ID to activate (e.g. \"balanced-recall-v1\").",
153 }],
154 },
155 HandlerDef {
156 name: "brain.deactivate",
157 description: "Move a profile to Inactive (stop live updates, retain state)",
158 visibility: Visibility::Verb,
159 category: VerbCategory::Commissive,
160 params: &[ParamDef {
161 name: "profile_id",
162 param_type: "string",
163 required: true,
164 description: "Profile ID to deactivate.",
165 }],
166 },
167 HandlerDef {
168 name: "brain.archive",
169 description: "Move a profile to Archived (read-only, audit-retained)",
170 visibility: Visibility::Verb,
171 category: VerbCategory::Declaration,
172 params: &[ParamDef {
173 name: "profile_id",
174 param_type: "string",
175 required: true,
176 description: "Profile ID to archive.",
177 }],
178 },
179 HandlerDef {
180 name: "brain.reset",
181 description: "Reset posteriors to priors (preserves event history)",
182 visibility: Visibility::Verb,
183 category: VerbCategory::Declaration,
184 params: &[ParamDef {
185 name: "profile_id",
186 param_type: "string",
187 required: false,
188 description: "Profile ID to reset (must exist and be active). Defaults to \"balanced-recall-v1\". Use brain.profiles() to list profiles.",
189 }],
190 },
191 HandlerDef {
192 name: "brain.feedback",
193 description: "Emit a FeedbackExplicit event into the shared log",
194 visibility: Visibility::Verb,
195 category: VerbCategory::Commissive,
196 params: &[
197 ParamDef {
198 name: "target_id",
199 param_type: "uuid",
200 required: true,
201 description: "UUID of the memory note or entity the feedback applies to.",
202 },
203 ParamDef {
204 name: "signal",
205 param_type: "string",
206 required: true,
207 description: "Feedback signal: \"useful\" | \"not_useful\" | \"wrong\".",
208 },
209 ParamDef {
210 name: "served_by_profile_id",
211 param_type: "string",
212 required: false,
213 description: "Profile ID that served the result being rated. Recorded in the event payload.",
214 },
215 ParamDef {
216 name: "section_signals",
217 param_type: "object",
218 required: false,
219 description: "Per-section feedback signals: {\"section_name\": \"useful\"|\"not_useful\"|\"wrong\"}. For knowledge_compose profiles.",
220 },
221 ],
222 },
223 HandlerDef {
224 name: "brain.auto_feedback",
225 description: "Emit implicit feedback for recall results supplied by an agent. \
226 Convenience verb: agents call this after memory.recall instead of constructing \
227 a brain.feedback call manually. Keeps memory and brain packs decoupled (#517).",
228 visibility: Visibility::Verb,
229 category: VerbCategory::Commissive,
230 params: &[
231 ParamDef {
232 name: "query",
233 param_type: "string",
234 required: true,
235 description: "Recall query that produced the results.",
236 },
237 ParamDef {
238 name: "results",
239 param_type: "array",
240 required: true,
241 description: "Recall result objects; the first object's note_id is credited.",
242 },
243 ParamDef {
244 name: "signal",
245 param_type: "string",
246 required: false,
247 description: "Feedback signal. Defaults to \"implicit_positive\".",
248 },
249 ParamDef {
250 name: "served_by_profile_id",
251 param_type: "string",
252 required: false,
253 description: "Profile ID that served the recall. Defaults like brain.feedback.",
254 },
255 ],
256 },
257 HandlerDef {
259 name: "brain.bind",
260 description: "Write a row in the profile resolution table",
261 visibility: Visibility::Verb,
262 category: VerbCategory::Declaration,
263 params: &[
264 ParamDef {
265 name: "profile_id",
266 param_type: "string",
267 required: true,
268 description: "Profile ID to bind (must exist).",
269 },
270 ParamDef {
271 name: "actor",
272 param_type: "string",
273 required: false,
274 description: "Actor identifier to match. Default \"*\" (all actors). Cannot contain \"*\" inside a real value.",
275 },
276 ParamDef {
277 name: "namespace",
278 param_type: "string",
279 required: false,
280 description: "Namespace to match. Default \"*\" (all namespaces).",
281 },
282 ParamDef {
283 name: "consumer_kind",
284 param_type: "string",
285 required: false,
286 description: "Verb / operation kind to match. Default \"*\" (all kinds).",
287 },
288 ParamDef {
289 name: "priority",
290 param_type: "integer",
291 required: false,
292 description: "Binding priority; higher wins when multiple bindings match (default 0).",
293 },
294 ],
295 },
296 HandlerDef {
297 name: "brain.unbind",
298 description: "Remove rows from the profile resolution table. At least one filter (profile_id, actor, namespace, or consumer_kind) is required.",
299 visibility: Visibility::Verb,
300 category: VerbCategory::Declaration,
301 params: &[
302 ParamDef {
303 name: "profile_id",
304 param_type: "string",
305 required: false,
306 description: "Remove bindings for this profile ID. All filters use AND semantics. At least one filter is required.",
307 },
308 ParamDef {
309 name: "actor",
310 param_type: "string",
311 required: false,
312 description: "Remove bindings for this actor.",
313 },
314 ParamDef {
315 name: "namespace",
316 param_type: "string",
317 required: false,
318 description: "Remove bindings for this namespace.",
319 },
320 ParamDef {
321 name: "consumer_kind",
322 param_type: "string",
323 required: false,
324 description: "Remove bindings for this consumer_kind.",
325 },
326 ],
327 },
328 HandlerDef {
329 name: "brain.bindings",
330 description: "List rows in the profile resolution table, optionally filtered",
331 visibility: Visibility::Verb,
332 category: VerbCategory::Assertive,
333 params: &[
334 ParamDef {
335 name: "profile_id",
336 param_type: "string",
337 required: false,
338 description: "Filter bindings by profile ID.",
339 },
340 ParamDef {
341 name: "actor",
342 param_type: "string",
343 required: false,
344 description: "Filter bindings by actor.",
345 },
346 ParamDef {
347 name: "namespace",
348 param_type: "string",
349 required: false,
350 description: "Filter bindings by namespace.",
351 },
352 ParamDef {
353 name: "consumer_kind",
354 param_type: "string",
355 required: false,
356 description: "Filter bindings by consumer_kind.",
357 },
358 ],
359 },
360 HandlerDef {
361 name: "brain.create_profile",
362 description: "Create a new brain profile with given name and optional seed priors",
363 visibility: Visibility::Verb,
364 category: VerbCategory::Declaration,
365 params: &[
366 ParamDef {
367 name: "name",
368 param_type: "string",
369 required: true,
370 description: "Profile ID / name (alphanumeric, hyphens allowed, e.g. \"my-profile-v1\"). Must be unique.",
371 },
372 ParamDef {
373 name: "description",
374 param_type: "string",
375 required: false,
376 description: "Human-readable description for this profile.",
377 },
378 ParamDef {
379 name: "consumer_kind",
380 param_type: "string",
381 required: false,
382 description: "Operation kind this profile targets (e.g. \"recall\"). Default \"recall\".",
383 },
384 ParamDef {
385 name: "seed_priors",
386 param_type: "object",
387 required: false,
388 description: "Seed priors object. For knowledge_compose: {\"section_posteriors\": {\"overview\": {\"alpha\": 2.0, \"beta\": 2.0}, ...}}. For recall: {\"relevance\": {\"alpha\": 7.0, \"beta\": 3.0}, ...}.",
389 },
390 ],
391 },
392 HandlerDef {
394 name: "brain.emit",
395 description: "Manually emit a feedback event (deprecated; use brain.feedback)",
396 visibility: Visibility::Subhandler,
397 category: VerbCategory::Commissive,
398 params: &[
399 ParamDef {
400 name: "target_id",
401 param_type: "uuid",
402 required: true,
403 description: "UUID of the record the feedback applies to.",
404 },
405 ParamDef {
406 name: "signal",
407 param_type: "string",
408 required: true,
409 description: "Feedback signal: \"useful\" | \"not_useful\" | \"wrong\". Deprecated: use brain.feedback instead.",
410 },
411 ParamDef {
412 name: "served_by_profile_id",
413 param_type: "string",
414 required: false,
415 description: "Profile ID that served the result.",
416 },
417 ],
418 },
419];
420
421pub struct BrainPack {
429 runtime: KhiveRuntime,
430 state: Mutex<BrainState>,
432 fold: BalancedRecallFold,
434 section_fold: SectionPosteriorFold,
436 persistence: Mutex<persist::PersistenceTracker>,
438}
439
440impl Pack for BrainPack {
441 const NAME: &'static str = "brain";
442 const NOTE_KINDS: &'static [&'static str] = &[];
443 const ENTITY_KINDS: &'static [&'static str] = &[];
444 const HANDLERS: &'static [HandlerDef] = BRAIN_HANDLERS;
445 const REQUIRES: &'static [&'static str] = &["kg"];
446}
447
448impl BrainPack {
449 pub fn new(runtime: KhiveRuntime) -> Self {
450 let fold = BalancedRecallFold::new(ENTITY_CACHE_CAPACITY);
451 let section_fold = SectionPosteriorFold::new();
452 let state = BrainState::new(ENTITY_CACHE_CAPACITY);
453 Self {
454 runtime,
455 state: Mutex::new(state),
456 fold,
457 section_fold,
458 persistence: Mutex::new(persist::PersistenceTracker::new()),
459 }
460 }
461
462 async fn ensure_loaded(&self, token: &NamespaceToken) -> Result<(), RuntimeError> {
463 persist::ensure_loaded(
464 &self.runtime,
465 token,
466 &self.persistence,
467 &self.state,
468 &self.fold,
469 &self.section_fold,
470 ENTITY_CACHE_CAPACITY,
471 )
472 .await
473 }
474
475 pub fn snapshot(&self) -> crate::state::BrainStateSnapshot {
477 self.state.lock().unwrap().to_snapshot()
478 }
479
480 async fn handle_state(&self, _params: Value) -> Result<Value, RuntimeError> {
483 let state = self.state.lock().unwrap();
484 let snapshot = state.to_snapshot();
485 serde_json::to_value(&snapshot).map_err(|e| RuntimeError::InvalidInput(e.to_string()))
486 }
487
488 async fn handle_config(&self, params: Value) -> Result<Value, RuntimeError> {
491 #[derive(Deserialize)]
492 #[serde(deny_unknown_fields)]
493 struct ConfigParams {
494 parameter: Option<String>,
495 }
496 let p: ConfigParams = serde_json::from_value(params)
497 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
498
499 let state = self.state.lock().unwrap();
500 let br = &state.balanced_recall;
501
502 let param_map = [
503 ("recall::relevance_weight", &br.relevance),
504 ("recall::salience_weight", &br.salience),
505 ("recall::temporal_weight", &br.temporal),
506 ];
507
508 match p.parameter {
509 Some(key) => {
510 let posterior = param_map
511 .iter()
512 .find(|(k, _)| *k == key)
513 .map(|(_, p)| *p)
514 .ok_or_else(|| {
515 RuntimeError::NotFound(format!(
516 "parameter {key:?}; valid: {}",
517 param_map
518 .iter()
519 .map(|(k, _)| *k)
520 .collect::<Vec<_>>()
521 .join(", ")
522 ))
523 })?;
524 Ok(json!({
525 "parameter": key,
526 "mean": posterior.mean(),
527 "variance": posterior.variance(),
528 "ess": posterior.effective_sample_size(),
529 "alpha": posterior.alpha,
530 "beta": posterior.beta,
531 }))
532 }
533 None => {
534 let configs: serde_json::Map<String, Value> = param_map
535 .iter()
536 .map(|(k, p)| {
537 (
538 (*k).to_owned(),
539 json!({
540 "mean": p.mean(),
541 "variance": p.variance(),
542 "ess": p.effective_sample_size(),
543 }),
544 )
545 })
546 .collect();
547 Ok(Value::Object(configs))
548 }
549 }
550 }
551
552 async fn handle_events(
555 &self,
556 token: &NamespaceToken,
557 params: Value,
558 ) -> Result<Value, RuntimeError> {
559 #[derive(Deserialize)]
560 #[serde(deny_unknown_fields)]
561 struct EventsParams {
562 limit: Option<u32>,
563 }
564 let p: EventsParams = serde_json::from_value(params)
565 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
566
567 let limit = p.limit.unwrap_or(20).min(100);
568 let ns = token.namespace().as_str().to_string();
569
570 let store = self.runtime.events(token)?;
571 let filter = EventFilter {
572 verbs: vec![
573 "recall".into(),
574 "search".into(),
575 "brain.feedback".into(),
576 "brain.emit".into(), "get".into(),
578 "remember".into(),
579 ],
580 ..EventFilter::default()
581 };
582 let _ = ns;
583 let page = store
584 .query_events(filter, PageRequest { offset: 0, limit })
585 .await
586 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
587
588 let events: Vec<Value> = page
589 .items
590 .iter()
591 .map(|e| {
592 json!({
593 "id": e.id.to_string(),
594 "verb": e.verb,
595 "outcome": e.outcome,
596 "target_id": e.target_id.map(|t| t.to_string()),
597 "duration_us": e.duration_us,
598 "created_at": micros_to_iso(e.created_at),
599 "payload": e.payload,
600 })
601 })
602 .collect();
603
604 Ok(json!({
605 "count": events.len(),
606 "events": events,
607 }))
608 }
609
610 async fn handle_profiles(&self, params: Value) -> Result<Value, RuntimeError> {
613 #[derive(Deserialize)]
614 #[serde(deny_unknown_fields)]
615 struct ProfilesParams {
616 lifecycle: Option<String>,
617 }
618 let p: ProfilesParams = serde_json::from_value(params)
619 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
620
621 let state = self.state.lock().unwrap();
622 let filter_lc: Option<ProfileLifecycle> = p
626 .lifecycle
627 .as_deref()
628 .map(|s| match s {
629 "active" => Ok(ProfileLifecycle::Active),
630 "inactive" => Ok(ProfileLifecycle::Inactive),
631 "archived" => Ok(ProfileLifecycle::Archived),
632 other => Err(RuntimeError::InvalidInput(format!(
633 "invalid lifecycle {other:?}; expected one of 'active', 'inactive', 'archived'"
634 ))),
635 })
636 .transpose()?;
637
638 let profiles: Vec<&ProfileRecord> = state
639 .profiles
640 .values()
641 .filter(|r| filter_lc.as_ref().is_none_or(|lc| &r.lifecycle == lc))
642 .collect();
643
644 let items: Vec<Value> = profiles
645 .iter()
646 .map(|r| {
647 json!({
648 "id": r.id,
649 "description": r.description,
650 "consumer_kind": r.consumer_kind,
651 "state_class": r.state_class,
652 "lifecycle": r.lifecycle,
653 "total_events": r.total_events,
654 "exploration_epoch": r.exploration_epoch,
655 "created_at": r.created_at,
656 })
657 })
658 .collect();
659
660 Ok(json!({ "count": items.len(), "profiles": items }))
661 }
662
663 async fn handle_profile(&self, params: Value) -> Result<Value, RuntimeError> {
666 #[derive(Deserialize)]
668 #[serde(deny_unknown_fields)]
669 struct ProfileParams {
670 profile_id: Option<String>,
671 id: Option<String>,
672 }
673 let p: ProfileParams = serde_json::from_value(params)
674 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
675 let profile_id = p
676 .profile_id
677 .or(p.id)
678 .ok_or_else(|| RuntimeError::InvalidInput("missing field `profile_id`".into()))?;
679
680 let state = self.state.lock().unwrap();
681 let record = state
682 .profiles
683 .get(&profile_id)
684 .ok_or_else(|| RuntimeError::NotFound(format!("profile {:?}", profile_id)))?;
685
686 let section_summary = if let Some(ss) = state.section_states.get(&profile_id) {
688 let weights = derive_deterministic_weights(ss);
689 let mut sections_json: serde_json::Map<String, Value> =
690 serde_json::Map::with_capacity(ss.posteriors.len());
691 for (section, posterior) in &ss.posteriors {
692 let w = weights.get(section).copied().unwrap_or(0.0);
693 sections_json.insert(
694 section.as_str().to_owned(),
695 json!({
696 "alpha": posterior.alpha,
697 "beta": posterior.beta,
698 "mean": posterior.mean(),
699 "variance": posterior.variance(),
700 "ess": posterior.effective_sample_size(),
701 "weight": w,
702 }),
703 );
704 }
705 Value::Object(sections_json)
706 } else {
707 Value::Null
708 };
709
710 Ok(json!({
711 "id": record.id,
712 "description": record.description,
713 "consumer_kind": record.consumer_kind,
714 "state_class": record.state_class,
715 "lifecycle": record.lifecycle,
716 "total_events": record.total_events,
717 "exploration_epoch": record.exploration_epoch,
718 "created_at": record.created_at,
719 "state_snapshot": record.state_snapshot,
720 "section_posteriors": section_summary,
721 }))
722 }
723
724 async fn handle_resolve(&self, params: Value) -> Result<Value, RuntimeError> {
727 #[derive(Deserialize)]
728 #[serde(deny_unknown_fields)]
729 struct ResolveParams {
730 actor: Option<String>,
731 namespace: Option<String>,
732 consumer_kind: String,
733 }
734 let p: ResolveParams = serde_json::from_value(params)
735 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
736
737 let state = self.state.lock().unwrap();
738 match state.resolve_with_match(p.actor.as_deref(), p.namespace.as_deref(), &p.consumer_kind)
741 {
742 Some((record, matched_kind)) => Ok(json!({
743 "resolved_profile_id": record.id,
744 "lifecycle": record.lifecycle,
745 "requested_consumer_kind": p.consumer_kind,
746 "matched_consumer_kind": matched_kind,
747 })),
748 None => Err(RuntimeError::NotFound(format!(
749 "no profile resolved for consumer_kind={:?}",
750 p.consumer_kind
751 ))),
752 }
753 }
754
755 async fn handle_activate(&self, params: Value) -> Result<Value, RuntimeError> {
758 self.set_lifecycle(params, ProfileLifecycle::Active).await
759 }
760
761 async fn handle_deactivate(&self, params: Value) -> Result<Value, RuntimeError> {
762 self.set_lifecycle(params, ProfileLifecycle::Inactive).await
763 }
764
765 async fn handle_archive(&self, params: Value) -> Result<Value, RuntimeError> {
766 self.set_lifecycle(params, ProfileLifecycle::Archived).await
767 }
768
769 async fn set_lifecycle(
770 &self,
771 params: Value,
772 lifecycle: ProfileLifecycle,
773 ) -> Result<Value, RuntimeError> {
774 #[derive(Deserialize)]
775 #[serde(deny_unknown_fields)]
776 struct LifecycleParams {
777 profile_id: String,
778 }
779 let p: LifecycleParams = serde_json::from_value(params)
780 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
781
782 let mut state = self.state.lock().unwrap();
783 let record = state
784 .profiles
785 .get_mut(&p.profile_id)
786 .ok_or_else(|| RuntimeError::NotFound(format!("profile {:?}", p.profile_id)))?;
787
788 match (&record.lifecycle, &lifecycle) {
797 (ProfileLifecycle::Archived, _) => {
799 return Err(RuntimeError::InvalidInput(format!(
800 "profile {:?} is archived; archive is terminal and no transition is permitted",
801 p.profile_id
802 )));
803 }
804 (ProfileLifecycle::Active, ProfileLifecycle::Archived) => {
806 return Err(RuntimeError::InvalidInput(format!(
807 "profile {:?} is active; deactivate it before archiving",
808 p.profile_id
809 )));
810 }
811 _ => {}
813 }
814
815 record.lifecycle = lifecycle.clone();
816 Ok(json!({
817 "profile_id": p.profile_id,
818 "lifecycle": lifecycle,
819 }))
820 }
821
822 async fn handle_reset(&self, params: Value) -> Result<Value, RuntimeError> {
825 #[derive(Deserialize)]
827 #[serde(deny_unknown_fields)]
828 struct ResetParams {
829 profile_id: Option<String>,
830 }
831 let p: ResetParams = serde_json::from_value(params)
832 .map_err(|e| RuntimeError::InvalidInput(format!("brain.reset: {e}")))?;
833 let profile_id = p
834 .profile_id
835 .unwrap_or_else(|| "balanced-recall-v1".to_string());
836 let mut state = self.state.lock().unwrap();
837
838 let lifecycle = state
840 .profiles
841 .get(&profile_id)
842 .ok_or_else(|| RuntimeError::NotFound(format!("profile {:?}", profile_id)))?
843 .lifecycle
844 .clone();
845
846 if lifecycle == ProfileLifecycle::Archived {
848 return Err(RuntimeError::InvalidInput(format!(
849 "profile {:?} is archived; archive is terminal and reset is not permitted",
850 profile_id
851 )));
852 }
853
854 if profile_id == "balanced-recall-v1" {
855 state.reset_posteriors();
856 sync_balanced_recall_record(&mut state);
859 } else if state.profile_states.contains_key(&profile_id) {
860 state.reset_profile_posteriors(&profile_id);
862 } else {
863 if let Some(record) = state.profiles.get_mut(&profile_id) {
866 record.exploration_epoch += 1;
867 }
868 }
869
870 let epoch = if profile_id == "balanced-recall-v1" {
871 state.balanced_recall.exploration_epoch
872 } else {
873 state.profiles[&profile_id].exploration_epoch
874 };
875
876 Ok(json!({
877 "reset": true,
878 "profile_id": profile_id,
879 "exploration_epoch": epoch,
880 }))
881 }
882
883 async fn handle_feedback(
886 &self,
887 token: &NamespaceToken,
888 params: Value,
889 ) -> Result<Value, RuntimeError> {
890 let feedback_start = Instant::now();
891
892 #[derive(Deserialize)]
893 #[serde(deny_unknown_fields)]
894 struct FeedbackParams {
895 target_id: String,
896 signal: String,
897 served_by_profile_id: Option<String>,
898 section_signals: Option<serde_json::Value>,
899 }
900 let p: FeedbackParams = serde_json::from_value(params)
901 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
902
903 let target: uuid::Uuid = p
904 .target_id
905 .parse()
906 .map_err(|e| RuntimeError::InvalidInput(format!("invalid target_id: {e}")))?;
907
908 let signal = match p.signal.as_str() {
909 "useful" => "useful",
910 "not_useful" => "not_useful",
911 "wrong" => "wrong",
912 "explicit_positive" => "explicit_positive",
914 "explicit_negative" => "explicit_negative",
915 "implicit_positive" => "implicit_positive",
916 "implicit_negative" => "implicit_negative",
917 "correction" => "correction",
918 other => {
919 return Err(RuntimeError::InvalidInput(format!(
920 "unknown signal {other:?}; valid: useful | not_useful | wrong | \
921 explicit_positive | explicit_negative | implicit_positive | \
922 implicit_negative | correction"
923 )))
924 }
925 };
926
927 let resolved = self
929 .runtime
930 .resolve(token, target)
931 .await
932 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
933 if resolved.is_none() {
934 return Err(RuntimeError::NotFound(format!(
935 "target_id {:?} not found in namespace {:?}",
936 target,
937 token.namespace().as_str()
938 )));
939 }
940
941 let effective_profile = p
944 .served_by_profile_id
945 .as_deref()
946 .unwrap_or("balanced-recall-v1");
947 {
948 let state = self.state.lock().unwrap();
949 match state.profiles.get(effective_profile) {
950 None => {
951 return Err(RuntimeError::NotFound(format!(
952 "serving profile {:?} not found in profile registry",
953 effective_profile
954 )));
955 }
956 Some(rec) if rec.lifecycle == crate::state::ProfileLifecycle::Archived => {
957 return Err(RuntimeError::InvalidInput(format!(
958 "serving profile {:?} is archived; feedback cannot credit archived profiles",
959 effective_profile
960 )));
961 }
962 Some(_) => {}
963 }
964 }
965
966 let mut data = json!({"signal": signal});
967 if let Some(ref profile_id) = p.served_by_profile_id {
968 data["served_by_profile_id"] = json!(profile_id);
969 }
970 if let Some(ref ss) = p.section_signals {
971 data["section_signals"] = ss.clone();
972 }
973
974 let duration_us = feedback_start.elapsed().as_micros().max(1) as i64;
975 let event = Event::new(
976 token.namespace().as_str().to_string(),
977 "brain.feedback",
978 khive_types::EventKind::FeedbackExplicit,
979 khive_types::SubstrateKind::Event,
980 "brain",
981 )
982 .with_target(target)
983 .with_payload(data)
984 .with_duration_us(duration_us);
985
986 let store = self.runtime.events(token)?;
987 store
988 .append_event(event.clone())
989 .await
990 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
991
992 let serving_profile_owned = p
993 .served_by_profile_id
994 .as_deref()
995 .unwrap_or("balanced-recall-v1")
996 .to_string();
997
998 {
999 let ctx = FoldContext::new();
1000 let mut state = self.state.lock().unwrap();
1001 let serving_profile = serving_profile_owned.as_str();
1002
1003 if serving_profile == "balanced-recall-v1" {
1004 let current_recall = std::mem::replace(
1005 &mut state.balanced_recall,
1006 crate::state::BalancedRecallState::new(0),
1007 );
1008 let updated = self.fold.reduce(current_recall, &event, &ctx);
1009 state.balanced_recall = updated;
1010 sync_balanced_recall_record(&mut state);
1011 } else if state.profile_states.contains_key(serving_profile) {
1012 let current = state
1013 .profile_states
1014 .remove(serving_profile)
1015 .expect("key checked above");
1016 let updated = self.fold.reduce(current, &event, &ctx);
1017 let snap = serde_json::to_value(updated.to_snapshot()).ok();
1018 let total = updated.total_events;
1019 state
1020 .profile_states
1021 .insert(serving_profile.to_string(), updated);
1022 if let Some(record) = state.profiles.get_mut(serving_profile) {
1023 record.total_events = total;
1024 record.state_snapshot = snap;
1025 }
1026 } else {
1027 let current_recall = std::mem::replace(
1028 &mut state.balanced_recall,
1029 crate::state::BalancedRecallState::new(0),
1030 );
1031 let updated = self.fold.reduce(current_recall, &event, &ctx);
1032 state.balanced_recall = updated;
1033 sync_balanced_recall_record(&mut state);
1034 }
1035
1036 if let Some(section_state) = state.section_states.remove(serving_profile) {
1037 let updated = self.section_fold.reduce(section_state, &event, &ctx);
1038 state
1039 .section_states
1040 .insert(serving_profile.to_string(), updated);
1041 }
1042 }
1043
1044 if let Err(e) = persist::persist_after_feedback(
1046 &self.runtime,
1047 token,
1048 &self.persistence,
1049 &self.state,
1050 &event,
1051 &serving_profile_owned,
1052 )
1053 .await
1054 {
1055 eprintln!("[brain] persistence failed (non-fatal): {e}");
1056 }
1057
1058 Ok(json!({
1059 "emitted": true,
1060 "event_id": event.id.to_string(),
1061 "verb": "brain.feedback",
1062 "signal": signal,
1063 "target_id": target.to_string(),
1064 }))
1065 }
1066
1067 async fn handle_auto_feedback(
1076 &self,
1077 token: &NamespaceToken,
1078 params: Value,
1079 ) -> Result<Value, RuntimeError> {
1080 #[derive(Deserialize)]
1081 #[serde(deny_unknown_fields)]
1082 struct AutoFeedbackParams {
1083 query: String,
1084 results: Vec<AutoFeedbackResult>,
1085 signal: Option<String>,
1086 served_by_profile_id: Option<String>,
1087 }
1088
1089 #[derive(Deserialize)]
1090 struct AutoFeedbackResult {
1091 note_id: String,
1092 }
1093
1094 let p: AutoFeedbackParams = serde_json::from_value(params)
1095 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
1096 if p.query.trim().is_empty() {
1097 return Err(RuntimeError::InvalidInput(
1098 "auto_feedback: `query` must not be empty".into(),
1099 ));
1100 }
1101
1102 let Some(first) = p.results.first() else {
1103 return Ok(json!({
1104 "emitted": false,
1105 "verb": "brain.auto_feedback",
1106 "reason": "no_results",
1107 }));
1108 };
1109
1110 let target = resolve_auto_feedback_target(&self.runtime, token, &first.note_id).await?;
1111
1112 let mut feedback_params = json!({
1113 "target_id": target.to_string(),
1114 "signal": p.signal.as_deref().unwrap_or("implicit_positive"),
1115 });
1116 if let Some(ref profile_id) = p.served_by_profile_id {
1117 feedback_params["served_by_profile_id"] = json!(profile_id);
1118 }
1119
1120 let mut out = self.handle_feedback(token, feedback_params).await?;
1121 out["verb"] = json!("brain.auto_feedback");
1122 out["feedback_verb"] = json!("brain.feedback");
1123 out["result_count"] = json!(p.results.len());
1124 Ok(out)
1125 }
1126
1127 async fn handle_emit(
1132 &self,
1133 token: &NamespaceToken,
1134 params: Value,
1135 ) -> Result<Value, RuntimeError> {
1136 self.handle_feedback(token, params).await
1137 }
1138
1139 async fn handle_bind(&self, params: Value) -> Result<Value, RuntimeError> {
1142 #[derive(Deserialize)]
1143 #[serde(deny_unknown_fields)]
1144 struct BindParams {
1145 profile_id: String,
1146 actor: Option<String>,
1147 namespace: Option<String>,
1148 consumer_kind: Option<String>,
1149 priority: Option<i32>,
1150 }
1151 let p: BindParams = serde_json::from_value(params)
1152 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
1153
1154 let mut state = self.state.lock().unwrap();
1155
1156 match state.profiles.get(&p.profile_id) {
1158 None => {
1159 return Err(RuntimeError::NotFound(format!(
1160 "profile {:?}",
1161 p.profile_id
1162 )));
1163 }
1164 Some(record) if record.lifecycle == ProfileLifecycle::Archived => {
1165 return Err(RuntimeError::InvalidInput(format!(
1166 "profile {:?} is archived; bindings to archived profiles are not permitted",
1167 p.profile_id
1168 )));
1169 }
1170 Some(_) => {}
1171 }
1172
1173 let actor = p.actor.unwrap_or_else(|| "*".into());
1174 let namespace = p.namespace.unwrap_or_else(|| "*".into());
1175 let consumer_kind = p.consumer_kind.unwrap_or_else(|| "*".into());
1176
1177 for (field, val) in [
1179 ("actor", &actor),
1180 ("namespace", &namespace),
1181 ("consumer_kind", &consumer_kind),
1182 ] {
1183 if val.as_str() != "*" && val.contains('*') {
1184 return Err(RuntimeError::InvalidInput(format!(
1185 "{field}: '*' is reserved as the wildcard sentinel and cannot appear inside a real value"
1186 )));
1187 }
1188 }
1189
1190 state.bindings.retain(|b| {
1192 !(b.actor == actor && b.namespace == namespace && b.consumer_kind == consumer_kind)
1193 });
1194
1195 state.bindings.push(ProfileBinding {
1196 actor: actor.clone(),
1197 namespace: namespace.clone(),
1198 consumer_kind: consumer_kind.clone(),
1199 profile_id: p.profile_id.clone(),
1200 priority: p.priority.unwrap_or(0),
1201 created_at: Utc::now(),
1202 });
1203
1204 Ok(json!({
1205 "bound": true,
1206 "profile_id": p.profile_id,
1207 "actor": actor,
1208 "namespace": namespace,
1209 "consumer_kind": consumer_kind,
1210 }))
1211 }
1212
1213 async fn handle_unbind(&self, params: Value) -> Result<Value, RuntimeError> {
1216 #[derive(Deserialize)]
1217 #[serde(deny_unknown_fields)]
1218 struct UnbindParams {
1219 profile_id: Option<String>,
1220 actor: Option<String>,
1221 namespace: Option<String>,
1222 consumer_kind: Option<String>,
1223 }
1224 let p: UnbindParams = serde_json::from_value(params)
1225 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
1226
1227 if p.profile_id.is_none()
1229 && p.actor.is_none()
1230 && p.namespace.is_none()
1231 && p.consumer_kind.is_none()
1232 {
1233 return Err(RuntimeError::InvalidInput(
1234 "unbind requires at least one filter; pass profile_id, actor, namespace, or consumer_kind".into(),
1235 ));
1236 }
1237
1238 let mut state = self.state.lock().unwrap();
1239 let before = state.bindings.len();
1240
1241 state.bindings.retain(|b| {
1242 let pid_match = p.profile_id.as_ref().is_none_or(|id| &b.profile_id == id);
1243 let actor_match = p.actor.as_ref().is_none_or(|a| &b.actor == a);
1244 let ns_match = p.namespace.as_ref().is_none_or(|n| &b.namespace == n);
1245 let kind_match = p
1246 .consumer_kind
1247 .as_ref()
1248 .is_none_or(|k| &b.consumer_kind == k);
1249 !(pid_match && actor_match && ns_match && kind_match)
1253 });
1254
1255 let removed = before - state.bindings.len();
1256 Ok(json!({ "unbound": removed }))
1257 }
1258
1259 async fn handle_bindings(&self, params: Value) -> Result<Value, RuntimeError> {
1262 #[derive(Deserialize)]
1264 struct BindingsParams {
1265 profile_id: Option<String>,
1266 actor: Option<String>,
1267 namespace: Option<String>,
1268 consumer_kind: Option<String>,
1269 }
1270 let p: BindingsParams = serde_json::from_value(params)
1271 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
1272
1273 let state = self.state.lock().unwrap();
1274 let rows: Vec<Value> = state
1275 .bindings
1276 .iter()
1277 .filter(|b| {
1278 p.profile_id.as_ref().is_none_or(|id| &b.profile_id == id)
1279 && p.actor.as_ref().is_none_or(|a| &b.actor == a)
1280 && p.namespace.as_ref().is_none_or(|n| &b.namespace == n)
1281 && p.consumer_kind
1282 .as_ref()
1283 .is_none_or(|k| &b.consumer_kind == k)
1284 })
1285 .map(|b| {
1286 json!({
1287 "profile_id": b.profile_id,
1288 "actor": b.actor,
1289 "namespace": b.namespace,
1290 "consumer_kind": b.consumer_kind,
1291 "priority": b.priority,
1292 "created_at": b.created_at,
1293 })
1294 })
1295 .collect();
1296
1297 Ok(json!({ "count": rows.len(), "bindings": rows }))
1298 }
1299
1300 async fn handle_create_profile(&self, params: Value) -> Result<Value, RuntimeError> {
1303 #[derive(Deserialize)]
1306 struct CreateProfileParams {
1307 name: String,
1308 description: Option<String>,
1309 consumer_kind: Option<String>,
1310 seed_priors: Option<serde_json::Value>,
1311 }
1312 let p: CreateProfileParams = serde_json::from_value(params)
1313 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?;
1314
1315 let name = p.name.trim().to_string();
1317 if name.is_empty() {
1318 return Err(RuntimeError::InvalidInput(
1319 "name must not be empty or whitespace-only".into(),
1320 ));
1321 }
1322 if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
1323 return Err(RuntimeError::InvalidInput(format!(
1324 "name {:?} is invalid; profile IDs must match [a-zA-Z0-9-]+ (alphanumeric and hyphens only)",
1325 name
1326 )));
1327 }
1328 let p_name = name;
1329
1330 let consumer_kind = p.consumer_kind.unwrap_or_else(|| "recall".into());
1332 let ck_trimmed = consumer_kind.trim();
1333 if ck_trimmed.is_empty() {
1334 return Err(RuntimeError::InvalidInput(
1335 "consumer_kind must not be empty or whitespace".into(),
1336 ));
1337 }
1338 if ck_trimmed == "*" {
1339 return Err(RuntimeError::InvalidInput(
1340 "consumer_kind '*' is the wildcard sentinel and is not permitted for profile creation; provide a specific operation kind (e.g. \"recall\", \"search\")"
1341 .into(),
1342 ));
1343 }
1344
1345 let description = p
1346 .description
1347 .unwrap_or_else(|| format!("User-created profile: {}", p_name));
1348
1349 let mut state = self.state.lock().unwrap();
1350
1351 if state.profiles.contains_key(&p_name) {
1352 return Err(RuntimeError::InvalidInput(format!(
1353 "profile {:?} already exists",
1354 p_name
1355 )));
1356 }
1357
1358 let ps = crate::state::BalancedRecallState::new(ENTITY_CACHE_CAPACITY);
1361 let snap = serde_json::to_value(ps.to_snapshot()).ok();
1362
1363 let record = ProfileRecord {
1364 id: p_name.clone(),
1365 description: description.clone(),
1366 consumer_kind: consumer_kind.clone(),
1367 state_class: "Bayesian".into(),
1368 lifecycle: ProfileLifecycle::Inactive,
1369 created_at: Utc::now(),
1370 state_snapshot: snap,
1371 total_events: 0,
1372 exploration_epoch: 0,
1373 };
1374
1375 let section_state = if let Some(ref seed) = p.seed_priors {
1378 if let Some(sp_obj) = seed.get("section_posteriors").and_then(|v| v.as_object()) {
1379 let mut priors = std::collections::HashMap::new();
1380 for (key, val) in sp_obj {
1381 let st: SectionType = key.parse().map_err(|_| {
1382 RuntimeError::InvalidInput(format!("unknown section type: {key:?}"))
1383 })?;
1384 let alpha = val.get("alpha").and_then(|v| v.as_f64()).ok_or_else(|| {
1385 RuntimeError::InvalidInput(format!(
1386 "missing or invalid alpha for section {key:?}"
1387 ))
1388 })?;
1389 let beta = val.get("beta").and_then(|v| v.as_f64()).ok_or_else(|| {
1390 RuntimeError::InvalidInput(format!(
1391 "missing or invalid beta for section {key:?}"
1392 ))
1393 })?;
1394 if alpha <= 0.0 || beta <= 0.0 {
1395 return Err(RuntimeError::InvalidInput(format!(
1396 "alpha and beta must be positive for section {key:?}; got alpha={alpha}, beta={beta}"
1397 )));
1398 }
1399 priors.insert(st, crate::state::BetaPosterior::new(alpha, beta));
1400 }
1401 SectionPosteriorState::from_priors(priors)
1402 } else {
1403 return Err(RuntimeError::InvalidInput(
1404 "seed_priors must contain a 'section_posteriors' object".into(),
1405 ));
1406 }
1407 } else {
1408 SectionPosteriorState::new()
1409 };
1410
1411 state.profiles.insert(p_name.clone(), record);
1412 state.profile_states.insert(p_name.clone(), ps);
1413 state.section_states.insert(p_name.clone(), section_state);
1414
1415 Ok(json!({
1416 "created": true,
1417 "profile_id": p_name,
1418 "consumer_kind": consumer_kind,
1419 "lifecycle": "inactive",
1420 "description": description,
1421 }))
1422 }
1423}
1424
1425struct BrainPackFactory;
1428
1429impl khive_runtime::PackFactory for BrainPackFactory {
1430 fn name(&self) -> &'static str {
1431 "brain"
1432 }
1433
1434 fn requires(&self) -> &'static [&'static str] {
1435 &["kg"]
1436 }
1437
1438 fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime> {
1439 Box::new(BrainPack::new(runtime))
1440 }
1441}
1442
1443inventory::submit! { khive_runtime::PackRegistration(&BrainPackFactory) }
1444
1445#[async_trait]
1448impl PackRuntime for BrainPack {
1449 fn name(&self) -> &str {
1450 <BrainPack as Pack>::NAME
1451 }
1452
1453 fn note_kinds(&self) -> &'static [&'static str] {
1454 <BrainPack as Pack>::NOTE_KINDS
1455 }
1456
1457 fn entity_kinds(&self) -> &'static [&'static str] {
1458 <BrainPack as Pack>::ENTITY_KINDS
1459 }
1460
1461 fn handlers(&self) -> &'static [HandlerDef] {
1462 BRAIN_HANDLERS
1463 }
1464
1465 fn requires(&self) -> &'static [&'static str] {
1466 <BrainPack as Pack>::REQUIRES
1467 }
1468
1469 async fn dispatch(
1470 &self,
1471 verb: &str,
1472 params: Value,
1473 _registry: &VerbRegistry,
1474 token: &NamespaceToken,
1475 ) -> Result<Value, RuntimeError> {
1476 self.ensure_loaded(token).await?;
1477 match verb {
1478 "brain.state" => self.handle_state(params).await,
1480 "brain.config" => self.handle_config(params).await,
1481 "brain.events" => self.handle_events(token, params).await,
1482 "brain.profiles" => self.handle_profiles(params).await,
1483 "brain.profile" => self.handle_profile(params).await,
1484 "brain.resolve" => self.handle_resolve(params).await,
1485 "brain.bindings" => self.handle_bindings(params).await,
1486 "brain.activate" => self.handle_activate(params).await,
1488 "brain.deactivate" => self.handle_deactivate(params).await,
1489 "brain.archive" => self.handle_archive(params).await,
1490 "brain.reset" => self.handle_reset(params).await,
1491 "brain.feedback" => self.handle_feedback(token, params).await,
1492 "brain.auto_feedback" => self.handle_auto_feedback(token, params).await,
1493 "brain.bind" => self.handle_bind(params).await,
1495 "brain.unbind" => self.handle_unbind(params).await,
1496 "brain.create_profile" => self.handle_create_profile(params).await,
1497 "brain.emit" => self.handle_emit(token, params).await,
1499 _ => Err(RuntimeError::InvalidInput(format!(
1500 "brain pack does not handle verb {verb:?}"
1501 ))),
1502 }
1503 }
1504}
1505
1506async fn resolve_auto_feedback_target(
1514 runtime: &KhiveRuntime,
1515 token: &NamespaceToken,
1516 raw: &str,
1517) -> Result<uuid::Uuid, RuntimeError> {
1518 if let Ok(uuid) = raw.parse::<uuid::Uuid>() {
1519 return Ok(uuid);
1520 }
1521 if raw.len() >= 8 && raw.chars().all(|c| c.is_ascii_hexdigit()) {
1522 return runtime
1523 .resolve_prefix(token, raw)
1524 .await
1525 .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?
1526 .ok_or_else(|| {
1527 RuntimeError::InvalidInput(format!(
1528 "auto_feedback: no record matches note_id prefix: {raw:?}"
1529 ))
1530 });
1531 }
1532 Err(RuntimeError::InvalidInput(format!(
1533 "auto_feedback: invalid note_id {raw:?}; expected full UUID or 8-char hex prefix"
1534 )))
1535}
1536
1537#[async_trait]
1546impl DispatchHook for BrainPack {
1547 async fn on_dispatch(&self, view: &EventView) {
1548 if view.event.verb.starts_with("brain.") {
1554 return;
1555 }
1556
1557 let ctx = FoldContext::new();
1558 let mut state = self.state.lock().unwrap();
1559 let current = std::mem::replace(
1560 &mut state.balanced_recall,
1561 crate::state::BalancedRecallState::new(0),
1562 );
1563 let updated = self.fold.reduce(current, &view.event, &ctx);
1564 state.balanced_recall = updated;
1565
1566 sync_balanced_recall_record(&mut state);
1569 }
1570}
1571
1572#[cfg(test)]
1575mod tests {
1576 use super::*;
1577 use khive_runtime::{Namespace, VerbRegistryBuilder};
1578 use serde_json::json;
1579
1580 fn make_pack() -> (BrainPack, KhiveRuntime) {
1581 let rt = KhiveRuntime::memory().expect("in-memory runtime");
1582 let pack = BrainPack::new(rt.clone());
1583 (pack, rt)
1584 }
1585
1586 fn empty_registry() -> VerbRegistry {
1587 VerbRegistryBuilder::new()
1588 .build()
1589 .expect("empty registry builds successfully")
1590 }
1591
1592 async fn create_test_entity(rt: &KhiveRuntime, token: &NamespaceToken) -> String {
1595 let entity = rt
1596 .create_entity(token, "concept", None, "test-target", None, None, vec![])
1597 .await
1598 .expect("create test entity");
1599 entity.id.to_string()
1600 }
1601
1602 #[tokio::test]
1603 async fn dispatch_unknown_verb_returns_invalid_input() {
1604 let (pack, rt) = make_pack();
1605 let registry = empty_registry();
1606 let err = pack
1607 .dispatch(
1608 "brain.unknown",
1609 json!({}),
1610 ®istry,
1611 &rt.authorize(Namespace::local()).unwrap(),
1612 )
1613 .await
1614 .unwrap_err();
1615 if let RuntimeError::InvalidInput(msg) = &err {
1616 assert!(
1617 msg.contains("brain.unknown"),
1618 "expected verb name in error: {msg}"
1619 );
1620 } else {
1621 panic!("expected InvalidInput, got {err:?}");
1622 }
1623 }
1624
1625 #[tokio::test]
1626 async fn dispatch_reset_returns_true_and_increments_epoch() {
1627 let (pack, rt) = make_pack();
1628 let registry = empty_registry();
1629 let result = pack
1630 .dispatch(
1631 "brain.reset",
1632 json!({"profile_id": "balanced-recall-v1"}),
1633 ®istry,
1634 &rt.authorize(Namespace::local()).unwrap(),
1635 )
1636 .await
1637 .unwrap();
1638 assert_eq!(result["reset"], json!(true));
1639 assert_eq!(result["exploration_epoch"], json!(1u64));
1640 assert_eq!(result["profile_id"], json!("balanced-recall-v1"));
1641 }
1642
1643 #[tokio::test]
1644 async fn dispatch_reset_no_args_resets_default_profile() {
1645 let (pack, rt) = make_pack();
1647 let registry = empty_registry();
1648 let result = pack
1649 .dispatch(
1650 "brain.reset",
1651 json!({}),
1652 ®istry,
1653 &rt.authorize(Namespace::local()).unwrap(),
1654 )
1655 .await
1656 .expect("reset with no args must succeed (defaults to balanced-recall-v1)");
1657 assert_eq!(result["reset"], json!(true));
1658 assert_eq!(result["profile_id"], json!("balanced-recall-v1"));
1659 }
1660
1661 #[tokio::test]
1662 async fn dispatch_reset_nonexistent_profile_returns_not_found() {
1663 let (pack, rt) = make_pack();
1664 let registry = empty_registry();
1665 let err = pack
1666 .dispatch(
1667 "brain.reset",
1668 json!({"profile_id": "ghost-profile"}),
1669 ®istry,
1670 &rt.authorize(Namespace::local()).unwrap(),
1671 )
1672 .await
1673 .unwrap_err();
1674 assert!(
1675 matches!(err, RuntimeError::NotFound(_)),
1676 "reset on nonexistent profile must return NotFound, got {err:?}"
1677 );
1678 }
1679
1680 #[tokio::test]
1681 async fn dispatch_reset_archived_profile_returns_invalid_input() {
1682 let (pack, rt) = make_pack();
1683 let registry = empty_registry();
1684 let token = rt.authorize(Namespace::local()).unwrap();
1685
1686 pack.dispatch(
1688 "brain.deactivate",
1689 json!({"profile_id": "balanced-recall-v1"}),
1690 ®istry,
1691 &token,
1692 )
1693 .await
1694 .unwrap();
1695 pack.dispatch(
1696 "brain.archive",
1697 json!({"profile_id": "balanced-recall-v1"}),
1698 ®istry,
1699 &token,
1700 )
1701 .await
1702 .unwrap();
1703
1704 let err = pack
1705 .dispatch(
1706 "brain.reset",
1707 json!({"profile_id": "balanced-recall-v1"}),
1708 ®istry,
1709 &token,
1710 )
1711 .await
1712 .unwrap_err();
1713 if let RuntimeError::InvalidInput(msg) = &err {
1714 assert!(
1715 msg.contains("archived") || msg.contains("terminal"),
1716 "reset on archived profile must mention 'archived' or 'terminal'; got: {msg}"
1717 );
1718 } else {
1719 panic!("reset on archived profile must return InvalidInput, got {err:?}");
1720 }
1721 }
1722
1723 #[tokio::test]
1724 async fn dispatch_feedback_invalid_signal_returns_invalid_input() {
1725 let (pack, rt) = make_pack();
1726 let registry = empty_registry();
1727 let target = "00000000-0000-0000-0000-000000000001";
1728 let err = pack
1729 .dispatch(
1730 "brain.feedback",
1731 json!({"target_id": target, "signal": "bad_signal"}),
1732 ®istry,
1733 &rt.authorize(Namespace::local()).unwrap(),
1734 )
1735 .await
1736 .unwrap_err();
1737 if let RuntimeError::InvalidInput(msg) = &err {
1738 assert!(
1739 msg.contains("bad_signal"),
1740 "expected signal name in error: {msg}"
1741 );
1742 assert!(
1743 msg.contains("valid"),
1744 "expected hint about valid values: {msg}"
1745 );
1746 } else {
1747 panic!("expected InvalidInput, got {err:?}");
1748 }
1749 }
1750
1751 #[tokio::test]
1752 async fn dispatch_state_returns_snapshot_fields() {
1753 let (pack, rt) = make_pack();
1754 let registry = empty_registry();
1755 let result = pack
1756 .dispatch(
1757 "brain.state",
1758 json!({}),
1759 ®istry,
1760 &rt.authorize(Namespace::local()).unwrap(),
1761 )
1762 .await
1763 .unwrap();
1764 assert!(result.get("profiles").is_some(), "missing profiles");
1765 assert!(
1766 result.get("balanced_recall").is_some(),
1767 "missing balanced_recall"
1768 );
1769 assert!(result.get("bindings").is_some(), "missing bindings");
1770 }
1771
1772 #[tokio::test]
1773 async fn dispatch_profiles_returns_default_profile() {
1774 let (pack, rt) = make_pack();
1775 let registry = empty_registry();
1776 let result = pack
1777 .dispatch(
1778 "brain.profiles",
1779 json!({}),
1780 ®istry,
1781 &rt.authorize(Namespace::local()).unwrap(),
1782 )
1783 .await
1784 .unwrap();
1785 let profiles = result["profiles"].as_array().unwrap();
1786 assert!(!profiles.is_empty(), "expected at least one profile");
1787 assert_eq!(profiles[0]["id"], json!("balanced-recall-v1"));
1788 }
1789
1790 #[tokio::test]
1791 async fn dispatch_profiles_filtered_by_lifecycle() {
1792 let (pack, rt) = make_pack();
1793 let registry = empty_registry();
1794 let result = pack
1795 .dispatch(
1796 "brain.profiles",
1797 json!({"lifecycle": "active"}),
1798 ®istry,
1799 &rt.authorize(Namespace::local()).unwrap(),
1800 )
1801 .await
1802 .unwrap();
1803 let profiles = result["profiles"].as_array().unwrap();
1804 for p in profiles {
1805 assert_eq!(p["lifecycle"], json!("active"));
1806 }
1807 }
1808
1809 #[tokio::test]
1810 async fn dispatch_profile_returns_profile_details() {
1811 let (pack, rt) = make_pack();
1812 let registry = empty_registry();
1813 let result = pack
1814 .dispatch(
1815 "brain.profile",
1816 json!({"id": "balanced-recall-v1"}),
1817 ®istry,
1818 &rt.authorize(Namespace::local()).unwrap(),
1819 )
1820 .await
1821 .unwrap();
1822 assert_eq!(result["id"], json!("balanced-recall-v1"));
1823 assert_eq!(result["state_class"], json!("Bayesian"));
1824 assert_eq!(result["consumer_kind"], json!("recall"));
1825 }
1826
1827 #[tokio::test]
1828 async fn dispatch_profile_not_found_returns_not_found() {
1829 let (pack, rt) = make_pack();
1830 let registry = empty_registry();
1831 let err = pack
1832 .dispatch(
1833 "brain.profile",
1834 json!({"id": "nonexistent"}),
1835 ®istry,
1836 &rt.authorize(Namespace::local()).unwrap(),
1837 )
1838 .await
1839 .unwrap_err();
1840 assert!(matches!(err, RuntimeError::NotFound(_)));
1841 }
1842
1843 #[tokio::test]
1844 async fn dispatch_resolve_returns_default_profile_for_recall() {
1845 let (pack, rt) = make_pack();
1846 let registry = empty_registry();
1847 let result = pack
1848 .dispatch(
1849 "brain.resolve",
1850 json!({"consumer_kind": "recall"}),
1851 ®istry,
1852 &rt.authorize(Namespace::local()).unwrap(),
1853 )
1854 .await
1855 .unwrap();
1856 assert_eq!(result["resolved_profile_id"], json!("balanced-recall-v1"));
1857 }
1858
1859 #[tokio::test]
1860 async fn dispatch_activate_and_deactivate_profile() {
1861 let (pack, rt) = make_pack();
1862 let registry = empty_registry();
1863 let token = rt.authorize(Namespace::local()).unwrap();
1864
1865 let result = pack
1867 .dispatch(
1868 "brain.deactivate",
1869 json!({"profile_id": "balanced-recall-v1"}),
1870 ®istry,
1871 &token,
1872 )
1873 .await
1874 .unwrap();
1875 assert_eq!(result["lifecycle"], json!("inactive"));
1876
1877 let state = pack
1879 .dispatch(
1880 "brain.profile",
1881 json!({"id": "balanced-recall-v1"}),
1882 ®istry,
1883 &token,
1884 )
1885 .await
1886 .unwrap();
1887 assert_eq!(state["lifecycle"], json!("inactive"));
1888
1889 let result = pack
1891 .dispatch(
1892 "brain.activate",
1893 json!({"profile_id": "balanced-recall-v1"}),
1894 ®istry,
1895 &token,
1896 )
1897 .await
1898 .unwrap();
1899 assert_eq!(result["lifecycle"], json!("active"));
1900 }
1901
1902 #[tokio::test]
1903 async fn dispatch_archive_profile() {
1904 let (pack, rt) = make_pack();
1905 let registry = empty_registry();
1906 let token = rt.authorize(Namespace::local()).unwrap();
1907
1908 pack.dispatch(
1910 "brain.deactivate",
1911 json!({"profile_id": "balanced-recall-v1"}),
1912 ®istry,
1913 &token,
1914 )
1915 .await
1916 .unwrap();
1917
1918 let result = pack
1919 .dispatch(
1920 "brain.archive",
1921 json!({"profile_id": "balanced-recall-v1"}),
1922 ®istry,
1923 &token,
1924 )
1925 .await
1926 .unwrap();
1927 assert_eq!(result["lifecycle"], json!("archived"));
1928 }
1929
1930 #[tokio::test]
1931 async fn dispatch_activate_nonexistent_profile_returns_not_found() {
1932 let (pack, rt) = make_pack();
1933 let registry = empty_registry();
1934 let err = pack
1935 .dispatch(
1936 "brain.activate",
1937 json!({"profile_id": "ghost-profile"}),
1938 ®istry,
1939 &rt.authorize(Namespace::local()).unwrap(),
1940 )
1941 .await
1942 .unwrap_err();
1943 assert!(matches!(err, RuntimeError::NotFound(_)));
1944 }
1945
1946 #[tokio::test]
1947 async fn dispatch_bind_and_resolve_explicit_binding() {
1948 let (pack, rt) = make_pack();
1949 let registry = empty_registry();
1950 let token = rt.authorize(Namespace::local()).unwrap();
1951
1952 let result = pack
1954 .dispatch(
1955 "brain.bind",
1956 json!({
1957 "profile_id": "balanced-recall-v1",
1958 "actor": "agent-x",
1959 "consumer_kind": "recall"
1960 }),
1961 ®istry,
1962 &token,
1963 )
1964 .await
1965 .unwrap();
1966 assert_eq!(result["bound"], json!(true));
1967 assert_eq!(result["actor"], json!("agent-x"));
1968
1969 let resolved = pack
1971 .dispatch(
1972 "brain.resolve",
1973 json!({"actor": "agent-x", "consumer_kind": "recall"}),
1974 ®istry,
1975 &token,
1976 )
1977 .await
1978 .unwrap();
1979 assert_eq!(resolved["resolved_profile_id"], json!("balanced-recall-v1"));
1980 }
1981
1982 #[tokio::test]
1983 async fn dispatch_bind_nonexistent_profile_returns_not_found() {
1984 let (pack, rt) = make_pack();
1985 let registry = empty_registry();
1986 let err = pack
1987 .dispatch(
1988 "brain.bind",
1989 json!({"profile_id": "ghost", "consumer_kind": "recall"}),
1990 ®istry,
1991 &rt.authorize(Namespace::local()).unwrap(),
1992 )
1993 .await
1994 .unwrap_err();
1995 assert!(matches!(err, RuntimeError::NotFound(_)));
1996 }
1997
1998 #[tokio::test]
1999 async fn dispatch_unbind_removes_binding() {
2000 let (pack, rt) = make_pack();
2001 let registry = empty_registry();
2002 let token = rt.authorize(Namespace::local()).unwrap();
2003
2004 pack.dispatch(
2006 "brain.bind",
2007 json!({"profile_id": "balanced-recall-v1", "actor": "agent-y", "consumer_kind": "recall"}),
2008 ®istry,
2009 &token,
2010 )
2011 .await
2012 .unwrap();
2013
2014 let result = pack
2016 .dispatch(
2017 "brain.unbind",
2018 json!({"actor": "agent-y"}),
2019 ®istry,
2020 &token,
2021 )
2022 .await
2023 .unwrap();
2024 assert_eq!(result["unbound"], json!(1u64));
2025 }
2026
2027 #[tokio::test]
2033 async fn ue5_h1_invalid_lifecycle_error_lists_only_public_states() {
2034 let (pack, rt) = make_pack();
2035 let registry = empty_registry();
2036 let token = rt.authorize(Namespace::local()).unwrap();
2037
2038 let err = pack
2039 .dispatch(
2040 "brain.profiles",
2041 json!({"lifecycle": "deleted"}),
2042 ®istry,
2043 &token,
2044 )
2045 .await
2046 .unwrap_err();
2047 if let RuntimeError::InvalidInput(msg) = &err {
2048 assert!(
2050 msg.contains("active"),
2051 "UE5-H1: error must list 'active'; got: {msg}"
2052 );
2053 assert!(
2054 msg.contains("inactive"),
2055 "UE5-H1: error must list 'inactive'; got: {msg}"
2056 );
2057 assert!(
2058 msg.contains("archived"),
2059 "UE5-H1: error must list 'archived'; got: {msg}"
2060 );
2061 assert!(
2063 !msg.contains("defined"),
2064 "UE5-H1: error must NOT expose internal 'defined' state; got: {msg}"
2065 );
2066 assert!(
2067 !msg.contains("registered"),
2068 "UE5-H1: error must NOT expose internal 'registered' state; got: {msg}"
2069 );
2070 } else {
2071 panic!("UE5-H1: expected InvalidInput, got {err:?}");
2072 }
2073 }
2074
2075 #[tokio::test]
2076 async fn ue5_h1_internal_lifecycle_values_rejected() {
2077 let (pack, rt) = make_pack();
2078 let registry = empty_registry();
2079 let token = rt.authorize(Namespace::local()).unwrap();
2080
2081 for internal_state in ["defined", "registered"] {
2082 let err = pack
2083 .dispatch(
2084 "brain.profiles",
2085 json!({"lifecycle": internal_state}),
2086 ®istry,
2087 &token,
2088 )
2089 .await
2090 .unwrap_err();
2091 assert!(
2092 matches!(err, RuntimeError::InvalidInput(_)),
2093 "UE5-H1: internal lifecycle '{internal_state}' must be rejected, got {err:?}"
2094 );
2095 }
2096 }
2097
2098 #[tokio::test]
2105 async fn b_c1_archived_activate_is_rejected() {
2106 let (pack, rt) = make_pack();
2107 let registry = empty_registry();
2108 let token = rt.authorize(Namespace::local()).unwrap();
2109
2110 pack.dispatch(
2112 "brain.deactivate",
2113 json!({"profile_id": "balanced-recall-v1"}),
2114 ®istry,
2115 &token,
2116 )
2117 .await
2118 .unwrap();
2119 pack.dispatch(
2120 "brain.archive",
2121 json!({"profile_id": "balanced-recall-v1"}),
2122 ®istry,
2123 &token,
2124 )
2125 .await
2126 .unwrap();
2127
2128 let err = pack
2130 .dispatch(
2131 "brain.activate",
2132 json!({"profile_id": "balanced-recall-v1"}),
2133 ®istry,
2134 &token,
2135 )
2136 .await
2137 .unwrap_err();
2138 if let RuntimeError::InvalidInput(msg) = &err {
2139 assert!(
2140 msg.contains("terminal") || msg.contains("archived"),
2141 "B-C1: error must mention 'terminal' or 'archived'; got: {msg}"
2142 );
2143 } else {
2144 panic!("B-C1: expected InvalidInput, got {err:?}");
2145 }
2146 }
2147
2148 #[tokio::test]
2149 async fn b_c1_archived_deactivate_is_rejected() {
2150 let (pack, rt) = make_pack();
2151 let registry = empty_registry();
2152 let token = rt.authorize(Namespace::local()).unwrap();
2153
2154 pack.dispatch(
2156 "brain.deactivate",
2157 json!({"profile_id": "balanced-recall-v1"}),
2158 ®istry,
2159 &token,
2160 )
2161 .await
2162 .unwrap();
2163 pack.dispatch(
2164 "brain.archive",
2165 json!({"profile_id": "balanced-recall-v1"}),
2166 ®istry,
2167 &token,
2168 )
2169 .await
2170 .unwrap();
2171
2172 let err = pack
2174 .dispatch(
2175 "brain.deactivate",
2176 json!({"profile_id": "balanced-recall-v1"}),
2177 ®istry,
2178 &token,
2179 )
2180 .await
2181 .unwrap_err();
2182 assert!(
2183 matches!(err, RuntimeError::InvalidInput(_)),
2184 "B-C1: deactivate on archived must return InvalidInput, got {err:?}"
2185 );
2186 }
2187
2188 #[tokio::test]
2189 async fn b_c1_active_to_archived_direct_is_rejected() {
2190 let (pack, rt) = make_pack();
2191 let registry = empty_registry();
2192 let token = rt.authorize(Namespace::local()).unwrap();
2193
2194 let err = pack
2196 .dispatch(
2197 "brain.archive",
2198 json!({"profile_id": "balanced-recall-v1"}),
2199 ®istry,
2200 &token,
2201 )
2202 .await
2203 .unwrap_err();
2204 if let RuntimeError::InvalidInput(msg) = &err {
2205 assert!(
2206 msg.contains("deactivate") || msg.contains("inactive"),
2207 "B-C1: active→archived error must hint at deactivate; got: {msg}"
2208 );
2209 } else {
2210 panic!("B-C1: expected InvalidInput for active→archived, got {err:?}");
2211 }
2212 }
2213
2214 #[tokio::test]
2215 async fn b_c1_inactive_to_archived_is_permitted() {
2216 let (pack, rt) = make_pack();
2217 let registry = empty_registry();
2218 let token = rt.authorize(Namespace::local()).unwrap();
2219
2220 pack.dispatch(
2222 "brain.deactivate",
2223 json!({"profile_id": "balanced-recall-v1"}),
2224 ®istry,
2225 &token,
2226 )
2227 .await
2228 .unwrap();
2229 let result = pack
2230 .dispatch(
2231 "brain.archive",
2232 json!({"profile_id": "balanced-recall-v1"}),
2233 ®istry,
2234 &token,
2235 )
2236 .await
2237 .unwrap();
2238 assert_eq!(
2239 result["lifecycle"],
2240 json!("archived"),
2241 "B-C1: inactive→archived must succeed"
2242 );
2243 }
2244
2245 #[tokio::test]
2248 async fn dispatch_unbind_uses_and_not_or() {
2249 let (pack, rt) = make_pack();
2250 let registry = empty_registry();
2251 let token = rt.authorize(Namespace::local()).unwrap();
2252
2253 pack.dispatch(
2255 "brain.bind",
2256 json!({"profile_id": "balanced-recall-v1", "namespace": "ns-a", "consumer_kind": "recall"}),
2257 ®istry,
2258 &token,
2259 )
2260 .await
2261 .unwrap();
2262
2263 pack.dispatch(
2265 "brain.bind",
2266 json!({"profile_id": "balanced-recall-v1", "namespace": "ns-b", "consumer_kind": "recall"}),
2267 ®istry,
2268 &token,
2269 )
2270 .await
2271 .unwrap();
2272
2273 let result = pack
2275 .dispatch(
2276 "brain.unbind",
2277 json!({"namespace": "ns-a", "profile_id": "balanced-recall-v1"}),
2278 ®istry,
2279 &token,
2280 )
2281 .await
2282 .unwrap();
2283 assert_eq!(
2284 result["unbound"],
2285 json!(1u64),
2286 "should remove exactly one binding"
2287 );
2288
2289 let state = pack.state.lock().unwrap();
2291 let remaining: Vec<_> = state
2292 .bindings
2293 .iter()
2294 .filter(|b| b.namespace == "ns-b")
2295 .collect();
2296 assert_eq!(remaining.len(), 1, "ns-b binding must survive the unbind");
2297 }
2298
2299 #[tokio::test]
2300 async fn dispatch_config_all_parameters() {
2301 let (pack, rt) = make_pack();
2302 let registry = empty_registry();
2303 let result = pack
2304 .dispatch(
2305 "brain.config",
2306 json!({}),
2307 ®istry,
2308 &rt.authorize(Namespace::local()).unwrap(),
2309 )
2310 .await
2311 .unwrap();
2312 let obj = result.as_object().unwrap();
2313 assert!(obj.contains_key("recall::relevance_weight"));
2314 assert!(obj.contains_key("recall::salience_weight"));
2315 assert!(obj.contains_key("recall::temporal_weight"));
2316 }
2317
2318 #[tokio::test]
2319 async fn dispatch_config_single_parameter() {
2320 let (pack, rt) = make_pack();
2321 let registry = empty_registry();
2322 let result = pack
2323 .dispatch(
2324 "brain.config",
2325 json!({"parameter": "recall::relevance_weight"}),
2326 ®istry,
2327 &rt.authorize(Namespace::local()).unwrap(),
2328 )
2329 .await
2330 .unwrap();
2331 assert_eq!(result["parameter"], json!("recall::relevance_weight"));
2332 let mean = result["mean"].as_f64().unwrap();
2334 assert!((mean - 0.7).abs() < 1e-6);
2335 }
2336
2337 #[tokio::test]
2350 async fn test_356_profile_record_total_events_synced_after_feedback() {
2351 let (pack, rt) = make_pack();
2352 let registry = empty_registry();
2353 let token = rt.authorize(Namespace::local()).unwrap();
2354 let target = create_test_entity(&rt, &token).await;
2355
2356 for _ in 0..3 {
2358 pack.dispatch(
2359 "brain.feedback",
2360 json!({"target_id": target, "signal": "useful"}),
2361 ®istry,
2362 &token,
2363 )
2364 .await
2365 .unwrap();
2366 }
2367
2368 let snap = pack.snapshot();
2369 let live_total = snap.balanced_recall.total_events;
2370
2371 let record_result = pack
2372 .dispatch(
2373 "brain.profile",
2374 json!({"id": "balanced-recall-v1"}),
2375 ®istry,
2376 &token,
2377 )
2378 .await
2379 .unwrap();
2380 let record_total = record_result["total_events"].as_u64().unwrap();
2381
2382 assert_eq!(
2383 live_total, record_total,
2384 "#356 part-A: profile_record.total_events ({record_total}) must equal \
2385 balanced_recall.total_events ({live_total}) after feedback calls"
2386 );
2387 assert_eq!(live_total, 3, "expected exactly 3 events from part A");
2388
2389 {
2394 let mut state = pack.state.lock().unwrap();
2395 state.balanced_recall.total_events += 7; }
2398
2399 let hook_event = {
2403 use khive_types::{EventKind, SubstrateKind};
2404 let mut e = khive_storage::event::Event::new(
2405 "local",
2406 "search",
2407 EventKind::Audit,
2408 SubstrateKind::Event,
2409 "kg",
2410 );
2411 e.outcome = khive_types::EventOutcome::Success;
2412 e
2413 };
2414 let hook_view = khive_runtime::EventView {
2415 event: hook_event,
2416 observations: Vec::new(),
2417 };
2418 pack.on_dispatch(&hook_view).await;
2419
2420 let after_hook = pack
2422 .dispatch(
2423 "brain.profile",
2424 json!({"id": "balanced-recall-v1"}),
2425 ®istry,
2426 &token,
2427 )
2428 .await
2429 .unwrap();
2430 let after_total = after_hook["total_events"].as_u64().unwrap();
2431 let live_after = pack.snapshot().balanced_recall.total_events;
2432 assert_eq!(
2433 after_total, live_after,
2434 "#356 part-B: on_dispatch sync must correct desync; \
2435 record shows {after_total}, live state shows {live_after}"
2436 );
2437 }
2438
2439 #[tokio::test]
2448 async fn test_357_feedback_no_double_count() {
2449 let (pack, rt) = make_pack();
2450 let registry = empty_registry();
2451 let token = rt.authorize(Namespace::local()).unwrap();
2452 let target = create_test_entity(&rt, &token).await;
2453
2454 pack.dispatch(
2456 "brain.feedback",
2457 json!({"target_id": target, "signal": "useful"}),
2458 ®istry,
2459 &token,
2460 )
2461 .await
2462 .unwrap();
2463
2464 assert_eq!(
2465 pack.snapshot().balanced_recall.total_events,
2466 1,
2467 "#357 pre-hook: handle_feedback must fold exactly once"
2468 );
2469
2470 let hook_event = {
2475 use khive_types::{EventKind, SubstrateKind};
2476 khive_storage::event::Event::new(
2477 "local",
2478 "brain.feedback",
2479 EventKind::FeedbackExplicit,
2480 SubstrateKind::Event,
2481 "brain",
2482 )
2483 };
2484 let hook_view = khive_runtime::EventView {
2485 event: hook_event,
2486 observations: Vec::new(),
2487 };
2488 pack.on_dispatch(&hook_view).await;
2489
2490 assert_eq!(
2491 pack.snapshot().balanced_recall.total_events,
2492 1,
2493 "#357: total_events must remain 1 after on_dispatch(brain.feedback); \
2494 guard absent if this reads 2"
2495 );
2496 }
2497
2498 #[tokio::test]
2509 async fn test_295_reset_restores_domain_priors_not_uniform() {
2510 let (pack, rt) = make_pack();
2511 let registry = empty_registry();
2512 let token = rt.authorize(Namespace::local()).unwrap();
2513
2514 let hook_event = |verb: &str| {
2518 use khive_types::{EventKind, SubstrateKind};
2519 let mut e = khive_storage::event::Event::new(
2520 "local",
2521 verb,
2522 EventKind::Audit,
2523 SubstrateKind::Event,
2524 "kg",
2525 );
2526 e.outcome = khive_types::EventOutcome::Success;
2527 e
2528 };
2529
2530 for _ in 0..4 {
2532 let view = khive_runtime::EventView {
2533 event: hook_event("search"),
2534 observations: Vec::new(),
2535 };
2536 pack.on_dispatch(&view).await;
2537 }
2538
2539 let target = create_test_entity(&rt, &token).await;
2542 for _ in 0..5 {
2543 pack.dispatch(
2544 "brain.feedback",
2545 json!({"target_id": target, "signal": "useful"}),
2546 ®istry,
2547 &token,
2548 )
2549 .await
2550 .unwrap();
2551 }
2552
2553 let before = pack.snapshot();
2555 assert!(
2556 before.balanced_recall.salience.alpha > 2.0,
2557 "salience.alpha must have grown past prior after useful feedback"
2558 );
2559 assert!(
2560 before.balanced_recall.total_events >= 9,
2561 "expected at least 9 total events (4 hook + 5 feedback), got {}",
2562 before.balanced_recall.total_events
2563 );
2564 let pre_reset_record = pack
2566 .dispatch(
2567 "brain.profile",
2568 json!({"id": "balanced-recall-v1"}),
2569 ®istry,
2570 &token,
2571 )
2572 .await
2573 .unwrap();
2574 assert_eq!(
2575 pre_reset_record["total_events"].as_u64().unwrap(),
2576 before.balanced_recall.total_events,
2577 "#295 pre-reset: profile record total_events out of sync before reset"
2578 );
2579
2580 let reset_result = pack
2582 .dispatch(
2583 "brain.reset",
2584 json!({"profile_id": "balanced-recall-v1"}),
2585 ®istry,
2586 &token,
2587 )
2588 .await
2589 .unwrap();
2590 assert_eq!(reset_result["reset"], json!(true));
2591
2592 let epoch_after = reset_result["exploration_epoch"].as_u64().unwrap();
2594 assert!(
2595 epoch_after > 0,
2596 "#295: exploration_epoch must increment after reset"
2597 );
2598
2599 let after = pack.snapshot();
2601
2602 assert!(
2604 (after.balanced_recall.salience.alpha - 2.0).abs() < 1e-12,
2605 "#295: salience.alpha must be 2.0 after reset, got {}",
2606 after.balanced_recall.salience.alpha
2607 );
2608 assert!(
2609 (after.balanced_recall.salience.beta - 8.0).abs() < 1e-12,
2610 "#295: salience.beta must be 8.0 after reset, got {}",
2611 after.balanced_recall.salience.beta
2612 );
2613
2614 assert!(
2616 (after.balanced_recall.temporal.alpha - 1.0).abs() < 1e-12,
2617 "#295: temporal.alpha must be 1.0 after reset, got {}",
2618 after.balanced_recall.temporal.alpha
2619 );
2620 assert!(
2621 (after.balanced_recall.temporal.beta - 9.0).abs() < 1e-12,
2622 "#295: temporal.beta must be 9.0 after reset, got {}",
2623 after.balanced_recall.temporal.beta
2624 );
2625
2626 assert!(
2628 (after.balanced_recall.relevance.alpha - 7.0).abs() < 1e-12,
2629 "#295: relevance.alpha must be 7.0 after reset"
2630 );
2631
2632 let record = pack
2636 .dispatch(
2637 "brain.profile",
2638 json!({"id": "balanced-recall-v1"}),
2639 ®istry,
2640 &token,
2641 )
2642 .await
2643 .unwrap();
2644
2645 let record_total = record["total_events"].as_u64().unwrap();
2648 assert_eq!(
2649 record_total, after.balanced_recall.total_events,
2650 "#295: profile record total_events ({record_total}) must match \
2651 live state ({}) after reset",
2652 after.balanced_recall.total_events
2653 );
2654
2655 let record_epoch = record["exploration_epoch"].as_u64().unwrap();
2657 assert_eq!(
2658 record_epoch, epoch_after,
2659 "#295: profile record exploration_epoch ({record_epoch}) must match \
2660 reset result ({epoch_after})"
2661 );
2662
2663 let snap = &record["state_snapshot"];
2665 let sal_alpha = snap["salience"]["alpha"].as_f64().unwrap();
2666 assert!(
2667 (sal_alpha - 2.0).abs() < 1e-12,
2668 "#295: brain.profile state_snapshot salience.alpha must be 2.0 after reset, \
2669 got {sal_alpha}"
2670 );
2671 }
2672
2673 #[tokio::test]
2675 async fn brain_reset_rejects_unknown_kwargs() {
2676 let (pack, rt) = make_pack();
2677 let registry = empty_registry();
2678 let token = rt.authorize(Namespace::local()).unwrap();
2679 let err = pack
2680 .dispatch(
2681 "brain.reset",
2682 json!({"unknownkw": "oops"}),
2683 ®istry,
2684 &token,
2685 )
2686 .await
2687 .unwrap_err();
2688 assert!(
2689 matches!(err, RuntimeError::InvalidInput(_)),
2690 "brain.reset with unknown kwargs must return InvalidInput, got: {err:?}"
2691 );
2692 if let RuntimeError::InvalidInput(msg) = &err {
2693 assert!(
2694 msg.contains("brain.reset"),
2695 "error message must mention brain.reset, got: {msg}"
2696 );
2697 }
2698 }
2699
2700 #[tokio::test]
2702 async fn brain_reset_accepts_empty_params() {
2703 let (pack, rt) = make_pack();
2704 let registry = empty_registry();
2705 let token = rt.authorize(Namespace::local()).unwrap();
2706 let result = pack
2707 .dispatch("brain.reset", json!({}), ®istry, &token)
2708 .await
2709 .expect("brain.reset() must succeed with empty params");
2710 assert_eq!(result["reset"], json!(true));
2711 }
2712
2713 #[tokio::test]
2721 async fn test_355_posteriors_update_after_dispatch_via_hook() {
2722 let (pack, _rt) = make_pack();
2723 let before = pack.snapshot();
2724 let tmp_alpha_before = before.balanced_recall.temporal.alpha;
2725 let tmp_beta_before = before.balanced_recall.temporal.beta;
2726
2727 let target_id = uuid::Uuid::new_v4();
2730 let fast_hit_event = {
2731 use khive_types::{EventKind, SubstrateKind};
2732 let mut e = khive_storage::event::Event::new(
2733 "local",
2734 "recall",
2735 EventKind::Audit,
2736 SubstrateKind::Event,
2737 "memory",
2738 );
2739 e.outcome = khive_types::EventOutcome::Success;
2740 e.target_id = Some(target_id);
2741 e.duration_us = 10_000; e
2743 };
2744 let view = khive_runtime::EventView {
2745 event: fast_hit_event,
2746 observations: Vec::new(),
2747 };
2748 pack.on_dispatch(&view).await;
2749
2750 let after_fast = pack.snapshot();
2751 assert!(
2752 (after_fast.balanced_recall.temporal.alpha - (tmp_alpha_before + 1.0)).abs() < 1e-12,
2753 "#355: fast recall hit must increment temporal.alpha via hook: expected {}, got {}",
2754 tmp_alpha_before + 1.0,
2755 after_fast.balanced_recall.temporal.alpha
2756 );
2757 assert!(
2758 (after_fast.balanced_recall.temporal.beta - tmp_beta_before).abs() < 1e-12,
2759 "#355: fast hit must NOT increment temporal.beta"
2760 );
2761
2762 let slow_hit_event = {
2764 use khive_types::{EventKind, SubstrateKind};
2765 let mut e = khive_storage::event::Event::new(
2766 "local",
2767 "recall",
2768 EventKind::Audit,
2769 SubstrateKind::Event,
2770 "memory",
2771 );
2772 e.outcome = khive_types::EventOutcome::Success;
2773 e.target_id = Some(target_id);
2774 e.duration_us = 100_000; e
2776 };
2777 let view2 = khive_runtime::EventView {
2778 event: slow_hit_event,
2779 observations: Vec::new(),
2780 };
2781 pack.on_dispatch(&view2).await;
2782
2783 let after_slow = pack.snapshot();
2784 assert!(
2785 (after_slow.balanced_recall.temporal.beta - (tmp_beta_before + 1.0)).abs() < 1e-12,
2786 "#355: slow recall hit must increment temporal.beta via hook: expected {}, got {}",
2787 tmp_beta_before + 1.0,
2788 after_slow.balanced_recall.temporal.beta
2789 );
2790
2791 let miss_event = {
2793 use khive_types::{EventKind, SubstrateKind};
2794 let mut e = khive_storage::event::Event::new(
2795 "local",
2796 "recall",
2797 EventKind::Audit,
2798 SubstrateKind::Event,
2799 "memory",
2800 );
2801 e.outcome = khive_types::EventOutcome::Success;
2802 e
2804 };
2805 let view3 = khive_runtime::EventView {
2806 event: miss_event,
2807 observations: Vec::new(),
2808 };
2809 pack.on_dispatch(&view3).await;
2810
2811 let after_miss = pack.snapshot();
2812 assert!(
2813 (after_miss.balanced_recall.temporal.beta - (tmp_beta_before + 2.0)).abs() < 1e-12,
2814 "#355: recall miss must further increment temporal.beta: expected {}, got {}",
2815 tmp_beta_before + 2.0,
2816 after_miss.balanced_recall.temporal.beta
2817 );
2818 }
2819
2820 #[tokio::test]
2824 async fn w4_c2_unbind_no_filter_is_rejected() {
2825 let (pack, rt) = make_pack();
2826 let registry = empty_registry();
2827 let token = rt.authorize(Namespace::local()).unwrap();
2828
2829 pack.dispatch(
2831 "brain.bind",
2832 json!({"profile_id": "balanced-recall-v1", "actor": "agent-z", "consumer_kind": "recall"}),
2833 ®istry,
2834 &token,
2835 )
2836 .await
2837 .unwrap();
2838
2839 let err = pack
2840 .dispatch("brain.unbind", json!({}), ®istry, &token)
2841 .await
2842 .unwrap_err();
2843 if let RuntimeError::InvalidInput(msg) = &err {
2844 assert!(
2845 msg.contains("filter") || msg.contains("profile_id") || msg.contains("actor"),
2846 "C2: zero-filter unbind must mention required filter; got: {msg}"
2847 );
2848 } else {
2849 panic!("C2: zero-filter unbind must return InvalidInput, got {err:?}");
2850 }
2851
2852 let state = pack.state.lock().unwrap();
2854 assert!(
2855 !state.bindings.is_empty(),
2856 "C2: binding must survive the rejected unbind"
2857 );
2858 }
2859
2860 #[tokio::test]
2862 async fn w4_c3_bind_archived_profile_is_rejected() {
2863 let (pack, rt) = make_pack();
2864 let registry = empty_registry();
2865 let token = rt.authorize(Namespace::local()).unwrap();
2866
2867 pack.dispatch(
2869 "brain.deactivate",
2870 json!({"profile_id": "balanced-recall-v1"}),
2871 ®istry,
2872 &token,
2873 )
2874 .await
2875 .unwrap();
2876 pack.dispatch(
2877 "brain.archive",
2878 json!({"profile_id": "balanced-recall-v1"}),
2879 ®istry,
2880 &token,
2881 )
2882 .await
2883 .unwrap();
2884
2885 let err = pack
2886 .dispatch(
2887 "brain.bind",
2888 json!({"profile_id": "balanced-recall-v1", "consumer_kind": "recall"}),
2889 ®istry,
2890 &token,
2891 )
2892 .await
2893 .unwrap_err();
2894 if let RuntimeError::InvalidInput(msg) = &err {
2895 assert!(
2896 msg.contains("archived"),
2897 "C3: bind to archived profile must mention 'archived'; got: {msg}"
2898 );
2899 } else {
2900 panic!("C3: bind to archived profile must return InvalidInput, got {err:?}");
2901 }
2902 }
2903
2904 #[tokio::test]
2906 async fn w4_c3_resolve_skips_archived_binding() {
2907 let (pack, rt) = make_pack();
2908 let registry = empty_registry();
2909 let token = rt.authorize(Namespace::local()).unwrap();
2910
2911 {
2914 let mut state = pack.state.lock().unwrap();
2915 state.bindings.push(crate::state::ProfileBinding {
2916 actor: "*".into(),
2917 namespace: "*".into(),
2918 consumer_kind: "recall".into(),
2919 profile_id: "balanced-recall-v1".into(),
2920 priority: 100,
2921 created_at: chrono::Utc::now(),
2922 });
2923 state
2925 .profiles
2926 .get_mut("balanced-recall-v1")
2927 .unwrap()
2928 .lifecycle = ProfileLifecycle::Archived;
2929 }
2930
2931 let err = pack
2933 .dispatch(
2934 "brain.resolve",
2935 json!({"consumer_kind": "recall"}),
2936 ®istry,
2937 &token,
2938 )
2939 .await
2940 .unwrap_err();
2941 assert!(
2942 matches!(err, RuntimeError::NotFound(_)),
2943 "C3: resolve with only archived binding must return NotFound, got {err:?}"
2944 );
2945 }
2946
2947 #[tokio::test]
2949 async fn w4_c4_feedback_rejects_nonexistent_target() {
2950 let (pack, rt) = make_pack();
2951 let registry = empty_registry();
2952 let token = rt.authorize(Namespace::local()).unwrap();
2953
2954 let err = pack
2955 .dispatch(
2956 "brain.feedback",
2957 json!({"target_id": "00000000-0000-0000-0000-000000000000", "signal": "useful"}),
2958 ®istry,
2959 &token,
2960 )
2961 .await
2962 .unwrap_err();
2963 assert!(
2964 matches!(err, RuntimeError::NotFound(_)),
2965 "C4: feedback with nonexistent target_id must return NotFound, got {err:?}"
2966 );
2967 }
2968
2969 #[tokio::test]
2971 async fn w4_c4_feedback_rejects_nonexistent_profile() {
2972 let (pack, rt) = make_pack();
2973 let registry = empty_registry();
2974 let token = rt.authorize(Namespace::local()).unwrap();
2975
2976 let target = create_test_entity(&rt, &token).await;
2978
2979 let err = pack
2980 .dispatch(
2981 "brain.feedback",
2982 json!({"target_id": target, "signal": "useful", "served_by_profile_id": "fake-profile-xyz"}),
2983 ®istry,
2984 &token,
2985 )
2986 .await
2987 .unwrap_err();
2988 assert!(
2989 matches!(err, RuntimeError::NotFound(_)),
2990 "C4: feedback with nonexistent served_by_profile_id must return NotFound, got {err:?}"
2991 );
2992 }
2993
2994 #[tokio::test]
2996 async fn w4_c4_feedback_accepts_valid_target_and_profile() {
2997 let (pack, rt) = make_pack();
2998 let registry = empty_registry();
2999 let token = rt.authorize(Namespace::local()).unwrap();
3000
3001 let target = create_test_entity(&rt, &token).await;
3002
3003 let result = pack
3004 .dispatch(
3005 "brain.feedback",
3006 json!({"target_id": target, "signal": "useful", "served_by_profile_id": "balanced-recall-v1"}),
3007 ®istry,
3008 &token,
3009 )
3010 .await
3011 .unwrap();
3012 assert_eq!(result["emitted"], json!(true));
3013 assert_eq!(result["signal"], json!("useful"));
3014 }
3015
3016 #[tokio::test]
3018 async fn w4_h1_create_profile_creates_new_profile() {
3019 let (pack, rt) = make_pack();
3020 let registry = empty_registry();
3021 let token = rt.authorize(Namespace::local()).unwrap();
3022
3023 let result = pack
3024 .dispatch(
3025 "brain.create_profile",
3026 json!({"name": "my-profile-v1", "consumer_kind": "search", "description": "Custom search profile"}),
3027 ®istry,
3028 &token,
3029 )
3030 .await
3031 .unwrap();
3032 assert_eq!(result["created"], json!(true));
3033 assert_eq!(result["profile_id"], json!("my-profile-v1"));
3034 assert_eq!(result["lifecycle"], json!("inactive"));
3035 assert_eq!(result["consumer_kind"], json!("search"));
3036
3037 let profiles = pack
3039 .dispatch("brain.profiles", json!({}), ®istry, &token)
3040 .await
3041 .unwrap();
3042 let ids: Vec<&str> = profiles["profiles"]
3043 .as_array()
3044 .unwrap()
3045 .iter()
3046 .filter_map(|p| p["id"].as_str())
3047 .collect();
3048 assert!(
3049 ids.contains(&"my-profile-v1"),
3050 "new profile must appear in brain.profiles"
3051 );
3052 }
3053
3054 #[tokio::test]
3056 async fn w4_h1_create_profile_duplicate_rejected() {
3057 let (pack, rt) = make_pack();
3058 let registry = empty_registry();
3059 let token = rt.authorize(Namespace::local()).unwrap();
3060
3061 let err = pack
3062 .dispatch(
3063 "brain.create_profile",
3064 json!({"name": "balanced-recall-v1"}),
3065 ®istry,
3066 &token,
3067 )
3068 .await
3069 .unwrap_err();
3070 assert!(
3071 matches!(err, RuntimeError::InvalidInput(_)),
3072 "H1: duplicate profile name must return InvalidInput, got {err:?}"
3073 );
3074 }
3075
3076 #[tokio::test]
3078 async fn w4_h2_bindings_lists_rows() {
3079 let (pack, rt) = make_pack();
3080 let registry = empty_registry();
3081 let token = rt.authorize(Namespace::local()).unwrap();
3082
3083 let result = pack
3085 .dispatch("brain.bindings", json!({}), ®istry, &token)
3086 .await
3087 .unwrap();
3088 assert_eq!(result["count"], json!(0u64));
3089 assert_eq!(result["bindings"], json!([]));
3090
3091 pack.dispatch(
3093 "brain.bind",
3094 json!({"profile_id": "balanced-recall-v1", "actor": "agent-a", "consumer_kind": "recall"}),
3095 ®istry,
3096 &token,
3097 )
3098 .await
3099 .unwrap();
3100
3101 let result2 = pack
3102 .dispatch("brain.bindings", json!({}), ®istry, &token)
3103 .await
3104 .unwrap();
3105 assert_eq!(result2["count"], json!(1u64));
3106 let rows = result2["bindings"].as_array().unwrap();
3107 assert_eq!(rows[0]["actor"], json!("agent-a"));
3108 assert_eq!(rows[0]["profile_id"], json!("balanced-recall-v1"));
3109 }
3110
3111 #[tokio::test]
3113 async fn w4_h2_bindings_filtered() {
3114 let (pack, rt) = make_pack();
3115 let registry = empty_registry();
3116 let token = rt.authorize(Namespace::local()).unwrap();
3117
3118 pack.dispatch("brain.bind", json!({"profile_id": "balanced-recall-v1", "actor": "agent-1", "consumer_kind": "recall"}), ®istry, &token).await.unwrap();
3119 pack.dispatch("brain.bind", json!({"profile_id": "balanced-recall-v1", "actor": "agent-2", "consumer_kind": "search"}), ®istry, &token).await.unwrap();
3120
3121 let result = pack
3122 .dispatch(
3123 "brain.bindings",
3124 json!({"actor": "agent-1"}),
3125 ®istry,
3126 &token,
3127 )
3128 .await
3129 .unwrap();
3130 assert_eq!(result["count"], json!(1u64));
3131 assert_eq!(result["bindings"][0]["actor"], json!("agent-1"));
3132 }
3133
3134 #[tokio::test]
3136 async fn w4_h3_resolve_returns_both_requested_and_matched_kind() {
3137 let (pack, rt) = make_pack();
3138 let registry = empty_registry();
3139 let token = rt.authorize(Namespace::local()).unwrap();
3140
3141 pack.dispatch(
3143 "brain.bind",
3144 json!({"profile_id": "balanced-recall-v1", "consumer_kind": "*", "priority": 1}),
3145 ®istry,
3146 &token,
3147 )
3148 .await
3149 .unwrap();
3150
3151 let result = pack
3153 .dispatch(
3154 "brain.resolve",
3155 json!({"consumer_kind": "search"}),
3156 ®istry,
3157 &token,
3158 )
3159 .await
3160 .unwrap();
3161
3162 assert_eq!(
3163 result["requested_consumer_kind"],
3164 json!("search"),
3165 "H3: requested_consumer_kind must equal the query"
3166 );
3167 assert_eq!(
3168 result["matched_consumer_kind"],
3169 json!("*"),
3170 "H3: matched_consumer_kind must show the wildcard binding"
3171 );
3172 assert_eq!(result["resolved_profile_id"], json!("balanced-recall-v1"));
3173 }
3174
3175 #[tokio::test]
3177 async fn w4_h3_resolve_exact_match_returns_exact_kind() {
3178 let (pack, rt) = make_pack();
3179 let registry = empty_registry();
3180 let token = rt.authorize(Namespace::local()).unwrap();
3181
3182 let result = pack
3184 .dispatch(
3185 "brain.resolve",
3186 json!({"consumer_kind": "recall"}),
3187 ®istry,
3188 &token,
3189 )
3190 .await
3191 .unwrap();
3192 assert_eq!(result["requested_consumer_kind"], json!("recall"));
3193 assert_eq!(result["matched_consumer_kind"], json!("recall"));
3194 }
3195
3196 #[tokio::test]
3198 async fn r2_archived_exact_binding_defers_to_live_wildcard() {
3199 let (pack, rt) = make_pack();
3200 let registry = empty_registry();
3201 let token = rt.authorize(Namespace::local()).unwrap();
3202
3203 pack.dispatch(
3205 "brain.create_profile",
3206 json!({"name": "search-v1", "consumer_kind": "search"}),
3207 ®istry,
3208 &token,
3209 )
3210 .await
3211 .unwrap();
3212 pack.dispatch(
3213 "brain.activate",
3214 json!({"profile_id": "search-v1"}),
3215 ®istry,
3216 &token,
3217 )
3218 .await
3219 .unwrap();
3220
3221 {
3223 let mut state = pack.state.lock().unwrap();
3224 state.bindings.push(crate::state::ProfileBinding {
3225 actor: "*".into(),
3226 namespace: "*".into(),
3227 consumer_kind: "search".into(),
3228 profile_id: "balanced-recall-v1".into(),
3229 priority: 100,
3230 created_at: chrono::Utc::now(),
3231 });
3232 state
3234 .profiles
3235 .get_mut("balanced-recall-v1")
3236 .unwrap()
3237 .lifecycle = crate::state::ProfileLifecycle::Archived;
3238 }
3239
3240 pack.dispatch(
3242 "brain.bind",
3243 json!({"profile_id": "search-v1", "consumer_kind": "*", "priority": 1}),
3244 ®istry,
3245 &token,
3246 )
3247 .await
3248 .unwrap();
3249
3250 let result = pack
3252 .dispatch(
3253 "brain.resolve",
3254 json!({"consumer_kind": "search"}),
3255 ®istry,
3256 &token,
3257 )
3258 .await
3259 .unwrap();
3260 assert_eq!(
3261 result["resolved_profile_id"],
3262 json!("search-v1"),
3263 "r2 fix 3: archived high-priority binding must not suppress the live wildcard binding"
3264 );
3265 }
3266
3267 #[tokio::test]
3269 async fn r2_feedback_rejects_archived_served_by_profile() {
3270 let (pack, rt) = make_pack();
3271 let registry = empty_registry();
3272 let token = rt.authorize(Namespace::local()).unwrap();
3273
3274 let target = create_test_entity(&rt, &token).await;
3275
3276 pack.dispatch(
3278 "brain.deactivate",
3279 json!({"profile_id": "balanced-recall-v1"}),
3280 ®istry,
3281 &token,
3282 )
3283 .await
3284 .unwrap();
3285 pack.dispatch(
3286 "brain.archive",
3287 json!({"profile_id": "balanced-recall-v1"}),
3288 ®istry,
3289 &token,
3290 )
3291 .await
3292 .unwrap();
3293
3294 let err = pack
3295 .dispatch(
3296 "brain.feedback",
3297 json!({
3298 "target_id": target,
3299 "signal": "useful",
3300 "served_by_profile_id": "balanced-recall-v1"
3301 }),
3302 ®istry,
3303 &token,
3304 )
3305 .await
3306 .unwrap_err();
3307 if let RuntimeError::InvalidInput(msg) = &err {
3308 assert!(
3309 msg.contains("archived"),
3310 "r2 fix 4: feedback to archived profile must mention 'archived'; got: {msg}"
3311 );
3312 } else {
3313 panic!("r2 fix 4: feedback to archived served_by_profile_id must return InvalidInput, got {err:?}");
3314 }
3315 }
3316
3317 #[tokio::test]
3319 async fn r2_create_profile_rejects_empty_consumer_kind() {
3320 let (pack, rt) = make_pack();
3321 let registry = empty_registry();
3322 let token = rt.authorize(Namespace::local()).unwrap();
3323
3324 let err = pack
3325 .dispatch(
3326 "brain.create_profile",
3327 json!({"name": "bad-profile", "consumer_kind": ""}),
3328 ®istry,
3329 &token,
3330 )
3331 .await
3332 .unwrap_err();
3333 assert!(
3334 matches!(err, RuntimeError::InvalidInput(_)),
3335 "r2 fix 5: empty consumer_kind must return InvalidInput, got {err:?}"
3336 );
3337 }
3338
3339 #[tokio::test]
3340 async fn r2_create_profile_rejects_wildcard_consumer_kind() {
3341 let (pack, rt) = make_pack();
3342 let registry = empty_registry();
3343 let token = rt.authorize(Namespace::local()).unwrap();
3344
3345 let err = pack
3346 .dispatch(
3347 "brain.create_profile",
3348 json!({"name": "wildcard-profile", "consumer_kind": "*"}),
3349 ®istry,
3350 &token,
3351 )
3352 .await
3353 .unwrap_err();
3354 if let RuntimeError::InvalidInput(msg) = &err {
3355 assert!(
3356 msg.contains("wildcard") || msg.contains("sentinel") || msg.contains("*"),
3357 "r2 fix 5: wildcard consumer_kind rejection must explain the issue; got: {msg}"
3358 );
3359 } else {
3360 panic!("r2 fix 5: wildcard consumer_kind must return InvalidInput, got {err:?}");
3361 }
3362 }
3363
3364 #[tokio::test]
3365 async fn r2_create_profile_rejects_whitespace_consumer_kind() {
3366 let (pack, rt) = make_pack();
3367 let registry = empty_registry();
3368 let token = rt.authorize(Namespace::local()).unwrap();
3369
3370 let err = pack
3371 .dispatch(
3372 "brain.create_profile",
3373 json!({"name": "ws-profile", "consumer_kind": " "}),
3374 ®istry,
3375 &token,
3376 )
3377 .await
3378 .unwrap_err();
3379 assert!(
3380 matches!(err, RuntimeError::InvalidInput(_)),
3381 "r2 fix 5: whitespace consumer_kind must return InvalidInput, got {err:?}"
3382 );
3383 }
3384
3385 #[tokio::test]
3387 async fn r2_bindings_and_semantics_multi_filter() {
3388 let (pack, rt) = make_pack();
3389 let registry = empty_registry();
3390 let token = rt.authorize(Namespace::local()).unwrap();
3391
3392 for name in ["alpha-v1", "beta-v1"] {
3394 pack.dispatch(
3395 "brain.create_profile",
3396 json!({"name": name, "consumer_kind": "recall"}),
3397 ®istry,
3398 &token,
3399 )
3400 .await
3401 .unwrap();
3402 pack.dispatch(
3403 "brain.activate",
3404 json!({"profile_id": name}),
3405 ®istry,
3406 &token,
3407 )
3408 .await
3409 .unwrap();
3410 }
3411
3412 pack.dispatch(
3414 "brain.bind",
3415 json!({"profile_id": "balanced-recall-v1", "actor": "agent-A", "namespace": "ns-1", "consumer_kind": "recall"}),
3416 ®istry,
3417 &token,
3418 )
3419 .await
3420 .unwrap();
3421 pack.dispatch(
3422 "brain.bind",
3423 json!({"profile_id": "alpha-v1", "actor": "agent-A", "namespace": "ns-2", "consumer_kind": "search"}),
3424 ®istry,
3425 &token,
3426 )
3427 .await
3428 .unwrap();
3429 pack.dispatch(
3430 "brain.bind",
3431 json!({"profile_id": "beta-v1", "actor": "agent-B", "namespace": "ns-1", "consumer_kind": "recall"}),
3432 ®istry,
3433 &token,
3434 )
3435 .await
3436 .unwrap();
3437
3438 let r1 = pack
3440 .dispatch(
3441 "brain.bindings",
3442 json!({"profile_id": "balanced-recall-v1", "namespace": "ns-1"}),
3443 ®istry,
3444 &token,
3445 )
3446 .await
3447 .unwrap();
3448 assert_eq!(
3449 r1["count"],
3450 json!(1u64),
3451 "AND filter profile_id+namespace must return 1 row"
3452 );
3453 assert_eq!(r1["bindings"][0]["profile_id"], json!("balanced-recall-v1"));
3454 assert_eq!(r1["bindings"][0]["namespace"], json!("ns-1"));
3455
3456 let r2 = pack
3458 .dispatch(
3459 "brain.bindings",
3460 json!({"actor": "agent-A", "consumer_kind": "search"}),
3461 ®istry,
3462 &token,
3463 )
3464 .await
3465 .unwrap();
3466 assert_eq!(
3467 r2["count"],
3468 json!(1u64),
3469 "AND filter actor+consumer_kind must return 1 row"
3470 );
3471 assert_eq!(r2["bindings"][0]["profile_id"], json!("alpha-v1"));
3472
3473 let r3 = pack
3475 .dispatch(
3476 "brain.bindings",
3477 json!({"actor": "agent-B", "consumer_kind": "search"}),
3478 ®istry,
3479 &token,
3480 )
3481 .await
3482 .unwrap();
3483 assert_eq!(
3484 r3["count"],
3485 json!(0u64),
3486 "AND filter with no matches must return count=0"
3487 );
3488 assert_eq!(r3["bindings"], json!([]));
3489 }
3490
3491 #[tokio::test]
3495 async fn r2_user_profile_reset_mutates_posteriors() {
3496 let (pack, rt) = make_pack();
3497 let registry = empty_registry();
3498 let token = rt.authorize(Namespace::local()).unwrap();
3499
3500 let target = create_test_entity(&rt, &token).await;
3501
3502 pack.dispatch(
3503 "brain.create_profile",
3504 json!({"name": "custom-v1", "consumer_kind": "recall"}),
3505 ®istry,
3506 &token,
3507 )
3508 .await
3509 .unwrap();
3510 pack.dispatch(
3511 "brain.activate",
3512 json!({"profile_id": "custom-v1"}),
3513 ®istry,
3514 &token,
3515 )
3516 .await
3517 .unwrap();
3518
3519 pack.dispatch(
3522 "brain.feedback",
3523 json!({"target_id": target, "signal": "useful", "served_by_profile_id": "custom-v1"}),
3524 ®istry,
3525 &token,
3526 )
3527 .await
3528 .unwrap();
3529
3530 let mutated = pack
3532 .dispatch(
3533 "brain.profile",
3534 json!({"profile_id": "custom-v1"}),
3535 ®istry,
3536 &token,
3537 )
3538 .await
3539 .unwrap();
3540 let salience_alpha_before = mutated["state_snapshot"]["salience"]["alpha"]
3541 .as_f64()
3542 .expect("state_snapshot.salience.alpha must be a number");
3543 assert!(
3544 salience_alpha_before > 2.0,
3545 "r3 fix 2: feedback must have moved salience alpha above prior 2.0; got {salience_alpha_before}"
3546 );
3547 let epoch_before = mutated["exploration_epoch"].as_u64().unwrap();
3548
3549 let reset_result = pack
3551 .dispatch(
3552 "brain.reset",
3553 json!({"profile_id": "custom-v1"}),
3554 ®istry,
3555 &token,
3556 )
3557 .await
3558 .unwrap();
3559 assert_eq!(reset_result["reset"], json!(true));
3560 assert_eq!(reset_result["profile_id"], json!("custom-v1"));
3561
3562 let after = pack
3564 .dispatch(
3565 "brain.profile",
3566 json!({"profile_id": "custom-v1"}),
3567 ®istry,
3568 &token,
3569 )
3570 .await
3571 .unwrap();
3572 let epoch_after = after["exploration_epoch"].as_u64().unwrap();
3573 assert!(
3574 epoch_after > epoch_before,
3575 "r3 fix 2: reset must increment exploration_epoch on user-created profile; before={epoch_before} after={epoch_after}"
3576 );
3577
3578 let snap = &after["state_snapshot"];
3583 assert!(
3584 !snap.is_null(),
3585 "r3 fix 2: state_snapshot must be non-null after reset"
3586 );
3587
3588 let rel_alpha = snap["relevance"]["alpha"]
3589 .as_f64()
3590 .expect("relevance.alpha");
3591 let rel_beta = snap["relevance"]["beta"].as_f64().expect("relevance.beta");
3592 assert!(
3593 (rel_alpha - 7.0).abs() < 1e-9 && (rel_beta - 3.0).abs() < 1e-9,
3594 "r3 fix 2: relevance must be Beta(7,3) after reset; got ({rel_alpha},{rel_beta})"
3595 );
3596
3597 let sal_alpha = snap["salience"]["alpha"].as_f64().expect("salience.alpha");
3598 let sal_beta = snap["salience"]["beta"].as_f64().expect("salience.beta");
3599 assert!(
3600 (sal_alpha - 2.0).abs() < 1e-9 && (sal_beta - 8.0).abs() < 1e-9,
3601 "r3 fix 2: salience must be Beta(2,8) after reset; got ({sal_alpha},{sal_beta})"
3602 );
3603
3604 let tmp_alpha = snap["temporal"]["alpha"].as_f64().expect("temporal.alpha");
3605 let tmp_beta = snap["temporal"]["beta"].as_f64().expect("temporal.beta");
3606 assert!(
3607 (tmp_alpha - 1.0).abs() < 1e-9 && (tmp_beta - 9.0).abs() < 1e-9,
3608 "r3 fix 2: temporal must be Beta(1,9) after reset; got ({tmp_alpha},{tmp_beta})"
3609 );
3610 }
3611
3612 #[tokio::test]
3614 async fn r2_user_profile_feedback_routes_to_profile_state() {
3615 let (pack, rt) = make_pack();
3616 let registry = empty_registry();
3617 let token = rt.authorize(Namespace::local()).unwrap();
3618
3619 let target = create_test_entity(&rt, &token).await;
3620
3621 pack.dispatch(
3622 "brain.create_profile",
3623 json!({"name": "custom-v1", "consumer_kind": "recall"}),
3624 ®istry,
3625 &token,
3626 )
3627 .await
3628 .unwrap();
3629 pack.dispatch(
3630 "brain.activate",
3631 json!({"profile_id": "custom-v1"}),
3632 ®istry,
3633 &token,
3634 )
3635 .await
3636 .unwrap();
3637
3638 let before = pack
3639 .dispatch(
3640 "brain.profile",
3641 json!({"profile_id": "custom-v1"}),
3642 ®istry,
3643 &token,
3644 )
3645 .await
3646 .unwrap();
3647 let events_before = before["total_events"].as_u64().unwrap();
3648
3649 pack.dispatch(
3650 "brain.feedback",
3651 json!({"target_id": target, "signal": "useful", "served_by_profile_id": "custom-v1"}),
3652 ®istry,
3653 &token,
3654 )
3655 .await
3656 .unwrap();
3657
3658 let after = pack
3659 .dispatch(
3660 "brain.profile",
3661 json!({"profile_id": "custom-v1"}),
3662 ®istry,
3663 &token,
3664 )
3665 .await
3666 .unwrap();
3667 let events_after = after["total_events"].as_u64().unwrap();
3668 assert!(
3669 events_after > events_before,
3670 "r2 fix 2: feedback routed to custom profile must increment its total_events; before={events_before} after={events_after}"
3671 );
3672 }
3673
3674 #[tokio::test]
3676 async fn w4_h4_profile_accepts_profile_id_and_id_alias() {
3677 let (pack, rt) = make_pack();
3678 let registry = empty_registry();
3679 let token = rt.authorize(Namespace::local()).unwrap();
3680
3681 let r1 = pack
3683 .dispatch(
3684 "brain.profile",
3685 json!({"profile_id": "balanced-recall-v1"}),
3686 ®istry,
3687 &token,
3688 )
3689 .await
3690 .unwrap();
3691 assert_eq!(r1["id"], json!("balanced-recall-v1"));
3692
3693 let r2 = pack
3695 .dispatch(
3696 "brain.profile",
3697 json!({"id": "balanced-recall-v1"}),
3698 ®istry,
3699 &token,
3700 )
3701 .await
3702 .unwrap();
3703 assert_eq!(r2["id"], json!("balanced-recall-v1"));
3704 }
3705
3706 #[tokio::test]
3711 async fn r3_feedback_default_profile_archived_rejected() {
3712 let (pack, rt) = make_pack();
3713 let registry = empty_registry();
3714 let token = rt.authorize(Namespace::local()).unwrap();
3715
3716 let target = create_test_entity(&rt, &token).await;
3717
3718 pack.dispatch(
3720 "brain.deactivate",
3721 json!({"profile_id": "balanced-recall-v1"}),
3722 ®istry,
3723 &token,
3724 )
3725 .await
3726 .unwrap();
3727
3728 pack.dispatch(
3730 "brain.archive",
3731 json!({"profile_id": "balanced-recall-v1"}),
3732 ®istry,
3733 &token,
3734 )
3735 .await
3736 .unwrap();
3737
3738 let snap_before = pack
3742 .dispatch(
3743 "brain.profile",
3744 json!({"profile_id": "balanced-recall-v1"}),
3745 ®istry,
3746 &token,
3747 )
3748 .await
3749 .unwrap();
3750 let events_before = snap_before["total_events"].as_u64().unwrap_or(0);
3751
3752 let log_before = pack
3753 .dispatch("brain.events", json!({"limit": 1000}), ®istry, &token)
3754 .await
3755 .unwrap();
3756 let log_count_before = log_before["events"]
3757 .as_array()
3758 .map(|a| a.len())
3759 .unwrap_or(0);
3760
3761 let err = pack
3763 .dispatch(
3764 "brain.feedback",
3765 json!({"target_id": target, "signal": "useful"}),
3766 ®istry,
3767 &token,
3768 )
3769 .await
3770 .unwrap_err();
3771 match &err {
3772 RuntimeError::InvalidInput(msg) => {
3773 assert!(
3774 msg.contains("archived"),
3775 "r3-1: error must mention 'archived'; got: {msg}"
3776 );
3777 }
3778 other => panic!("r3-1: expected InvalidInput(archived), got {other:?}"),
3779 }
3780
3781 let snap_after = pack
3783 .dispatch(
3784 "brain.profile",
3785 json!({"profile_id": "balanced-recall-v1"}),
3786 ®istry,
3787 &token,
3788 )
3789 .await
3790 .unwrap();
3791 let events_after = snap_after["total_events"].as_u64().unwrap_or(0);
3792 assert_eq!(
3793 events_after, events_before,
3794 "r3-1: archived default profile must not have events appended; before={events_before} after={events_after}"
3795 );
3796
3797 let log_after = pack
3802 .dispatch("brain.events", json!({"limit": 1000}), ®istry, &token)
3803 .await
3804 .unwrap();
3805 let log_count_after = log_after["events"].as_array().map(|a| a.len()).unwrap_or(0);
3806 assert_eq!(
3807 log_count_after, log_count_before,
3808 "r3-1: rejected feedback must not append a FeedbackExplicit event; before={log_count_before} after={log_count_after}"
3809 );
3810 }
3811
3812 #[tokio::test]
3814 async fn r3_create_profile_id_grammar_enforced() {
3815 let (pack, rt) = make_pack();
3816 let registry = empty_registry();
3817 let token = rt.authorize(Namespace::local()).unwrap();
3818
3819 let err = pack
3821 .dispatch(
3822 "brain.create_profile",
3823 json!({"name": " ", "consumer_kind": "recall"}),
3824 ®istry,
3825 &token,
3826 )
3827 .await
3828 .unwrap_err();
3829 assert!(
3830 matches!(err, RuntimeError::InvalidInput(_)),
3831 "r3-3: whitespace-only name must return InvalidInput; got {err:?}"
3832 );
3833
3834 pack.dispatch(
3836 "brain.create_profile",
3837 json!({"name": " my-profile ", "consumer_kind": "recall"}),
3838 ®istry,
3839 &token,
3840 )
3841 .await
3842 .expect("r3-3: name with leading/trailing spaces should be accepted after trim");
3843
3844 let err = pack
3846 .dispatch(
3847 "brain.create_profile",
3848 json!({"name": "bad.profile", "consumer_kind": "recall"}),
3849 ®istry,
3850 &token,
3851 )
3852 .await
3853 .unwrap_err();
3854 assert!(
3855 matches!(err, RuntimeError::InvalidInput(_)),
3856 "r3-3: dot in name must return InvalidInput; got {err:?}"
3857 );
3858
3859 let err = pack
3861 .dispatch(
3862 "brain.create_profile",
3863 json!({"name": "bad_profile", "consumer_kind": "recall"}),
3864 ®istry,
3865 &token,
3866 )
3867 .await
3868 .unwrap_err();
3869 assert!(
3870 matches!(err, RuntimeError::InvalidInput(_)),
3871 "r3-3: underscore in name must return InvalidInput; got {err:?}"
3872 );
3873
3874 let err = pack
3876 .dispatch(
3877 "brain.create_profile",
3878 json!({"name": "*", "consumer_kind": "recall"}),
3879 ®istry,
3880 &token,
3881 )
3882 .await
3883 .unwrap_err();
3884 assert!(
3885 matches!(err, RuntimeError::InvalidInput(_)),
3886 "r3-3: asterisk name must return InvalidInput; got {err:?}"
3887 );
3888
3889 pack.dispatch(
3891 "brain.create_profile",
3892 json!({"name": "valid-profile-123", "consumer_kind": "recall"}),
3893 ®istry,
3894 &token,
3895 )
3896 .await
3897 .expect("r3-3: valid alphanumeric-hyphen name must succeed");
3898 }
3899
3900 #[tokio::test]
3902 async fn test_289_feedback_event_records_nonzero_duration() {
3903 let (pack, rt) = make_pack();
3904 let registry = empty_registry();
3905 let token = rt.authorize(Namespace::local()).unwrap();
3906 let target = create_test_entity(&rt, &token).await;
3907
3908 let result = pack
3909 .dispatch(
3910 "brain.feedback",
3911 json!({"target_id": target, "signal": "useful"}),
3912 ®istry,
3913 &token,
3914 )
3915 .await
3916 .unwrap();
3917 let event_id = result["event_id"].as_str().unwrap().to_string();
3918
3919 let log = pack
3920 .dispatch("brain.events", json!({"limit": 100}), ®istry, &token)
3921 .await
3922 .unwrap();
3923 let event = log["events"]
3924 .as_array()
3925 .unwrap()
3926 .iter()
3927 .find(|e| e["id"].as_str() == Some(event_id.as_str()))
3928 .expect("#289: feedback event must appear in brain.events");
3929
3930 assert!(
3931 event["duration_us"].as_i64().unwrap() > 0,
3932 "#289: feedback event duration_us must be non-zero, got {}",
3933 event["duration_us"]
3934 );
3935 }
3936
3937 #[tokio::test]
3940 async fn brain_auto_feedback_emits_implicit_positive_for_first_result() {
3941 let (pack, rt) = make_pack();
3942 let registry = empty_registry();
3943 let token = rt.authorize(Namespace::local()).unwrap();
3944 let target = create_test_entity(&rt, &token).await;
3945
3946 let result = pack
3947 .dispatch(
3948 "brain.auto_feedback",
3949 json!({
3950 "query": "recall calibration target",
3951 "results": [{ "note_id": target }]
3952 }),
3953 ®istry,
3954 &token,
3955 )
3956 .await
3957 .expect("auto_feedback succeeds");
3958
3959 assert_eq!(result["emitted"], json!(true), "emitted must be true");
3960 assert_eq!(
3961 result["signal"],
3962 json!("implicit_positive"),
3963 "default signal must be implicit_positive"
3964 );
3965 let returned_target_id = result["target_id"].as_str().unwrap_or("");
3966 assert_eq!(
3967 returned_target_id.len(),
3968 36,
3969 "target_id in auto_feedback response must be full 36-char UUID"
3970 );
3971 assert_eq!(
3972 returned_target_id, target,
3973 "target_id must match the created entity"
3974 );
3975 assert_eq!(
3976 pack.snapshot().balanced_recall.total_events,
3977 1,
3978 "auto_feedback must increment total_events"
3979 );
3980 }
3981
3982 #[tokio::test]
3983 async fn brain_auto_feedback_empty_results_returns_no_emit() {
3984 let (pack, rt) = make_pack();
3985 let registry = empty_registry();
3986 let token = rt.authorize(Namespace::local()).unwrap();
3987
3988 let result = pack
3989 .dispatch(
3990 "brain.auto_feedback",
3991 json!({
3992 "query": "empty recall results",
3993 "results": []
3994 }),
3995 ®istry,
3996 &token,
3997 )
3998 .await
3999 .expect("auto_feedback with empty results succeeds");
4000
4001 assert_eq!(result["emitted"], json!(false));
4002 assert_eq!(result["reason"], json!("no_results"));
4003 }
4004
4005 #[tokio::test]
4006 async fn brain_auto_feedback_accepts_short_note_id_prefix() {
4007 let (pack, rt) = make_pack();
4008 let registry = empty_registry();
4009 let token = rt.authorize(Namespace::local()).unwrap();
4010 let target = create_test_entity(&rt, &token).await;
4011 let prefix = &target[..8];
4013
4014 let result = pack
4015 .dispatch(
4016 "brain.auto_feedback",
4017 json!({
4018 "query": "prefix resolution test",
4019 "results": [{ "note_id": prefix }]
4020 }),
4021 ®istry,
4022 &token,
4023 )
4024 .await
4025 .expect("auto_feedback with 8-char prefix succeeds");
4026
4027 assert_eq!(result["emitted"], json!(true));
4028 assert_eq!(result["target_id"].as_str().unwrap_or("").len(), 36);
4029 }
4030}
4031
4032#[cfg(test)]
4033mod help_tests {
4034 use super::*;
4035
4036 fn find_handler(name: &str) -> &'static HandlerDef {
4037 BRAIN_HANDLERS
4038 .iter()
4039 .find(|h| h.name == name)
4040 .unwrap_or_else(|| panic!("handler {name:?} not found in BRAIN_HANDLERS"))
4041 }
4042
4043 #[test]
4044 fn brain_feedback_params_non_empty_and_has_target_and_signal() {
4045 let h = find_handler("brain.feedback");
4046 assert!(!h.params.is_empty(), "brain.feedback must have params");
4047 assert!(
4048 h.params.iter().any(|p| p.name == "target_id" && p.required),
4049 "brain.feedback must have required target_id param"
4050 );
4051 assert!(
4052 h.params.iter().any(|p| p.name == "signal" && p.required),
4053 "brain.feedback must have required signal param"
4054 );
4055 assert!(
4056 h.params.iter().any(|p| p.name == "served_by_profile_id"),
4057 "brain.feedback must document served_by_profile_id"
4058 );
4059 }
4060
4061 #[test]
4062 fn brain_auto_feedback_handler_is_declared() {
4063 let h = find_handler("brain.auto_feedback");
4064 assert!(
4065 h.params.iter().any(|p| p.name == "query" && p.required),
4066 "brain.auto_feedback must have required query param"
4067 );
4068 assert!(
4069 h.params.iter().any(|p| p.name == "results" && p.required),
4070 "brain.auto_feedback must have required results param"
4071 );
4072 }
4073
4074 #[test]
4075 fn brain_profile_params_has_required_profile_id() {
4076 let h = find_handler("brain.profile");
4077 assert!(!h.params.is_empty(), "brain.profile must have params");
4078 assert!(
4080 h.params
4081 .iter()
4082 .any(|p| p.name == "profile_id" && p.required),
4083 "brain.profile must have required profile_id param (H4 fix)"
4084 );
4085 }
4086
4087 #[test]
4088 fn brain_profiles_params_has_lifecycle_filter() {
4089 let h = find_handler("brain.profiles");
4090 assert!(!h.params.is_empty(), "brain.profiles must have params");
4091 assert!(
4092 h.params.iter().any(|p| p.name == "lifecycle"),
4093 "brain.profiles must document lifecycle filter param"
4094 );
4095 }
4096
4097 #[test]
4098 fn brain_resolve_params_has_consumer_kind_required() {
4099 let h = find_handler("brain.resolve");
4100 assert!(!h.params.is_empty(), "brain.resolve must have params");
4101 assert!(
4102 h.params
4103 .iter()
4104 .any(|p| p.name == "consumer_kind" && p.required),
4105 "brain.resolve must have required consumer_kind"
4106 );
4107 assert!(
4108 h.params.iter().any(|p| p.name == "actor"),
4109 "brain.resolve must document optional actor"
4110 );
4111 assert!(
4112 h.params.iter().any(|p| p.name == "namespace"),
4113 "brain.resolve must document optional namespace"
4114 );
4115 }
4116
4117 #[test]
4118 fn brain_bind_params_has_required_profile_id_and_optionals() {
4119 let h = find_handler("brain.bind");
4120 assert!(!h.params.is_empty(), "brain.bind must have params");
4121 assert!(
4122 h.params
4123 .iter()
4124 .any(|p| p.name == "profile_id" && p.required),
4125 "brain.bind must have required profile_id"
4126 );
4127 assert!(
4128 h.params.iter().any(|p| p.name == "actor"),
4129 "brain.bind must document actor"
4130 );
4131 assert!(
4132 h.params.iter().any(|p| p.name == "namespace"),
4133 "brain.bind must document namespace"
4134 );
4135 assert!(
4136 h.params.iter().any(|p| p.name == "consumer_kind"),
4137 "brain.bind must document consumer_kind"
4138 );
4139 assert!(
4140 h.params.iter().any(|p| p.name == "priority"),
4141 "brain.bind must document priority"
4142 );
4143 }
4144
4145 #[test]
4146 fn brain_unbind_params_non_empty_all_optional() {
4147 let h = find_handler("brain.unbind");
4148 assert!(!h.params.is_empty(), "brain.unbind must have params");
4149 assert!(
4150 h.params.iter().all(|p| !p.required),
4151 "brain.unbind params must all be optional (filter semantics)"
4152 );
4153 assert!(
4154 h.params.iter().any(|p| p.name == "profile_id"),
4155 "brain.unbind must document profile_id filter"
4156 );
4157 assert!(
4158 h.params.iter().any(|p| p.name == "actor"),
4159 "brain.unbind must document actor filter"
4160 );
4161 }
4162
4163 #[test]
4164 fn brain_activate_deactivate_archive_each_have_profile_id() {
4165 for verb in ["brain.activate", "brain.deactivate", "brain.archive"] {
4166 let h = find_handler(verb);
4167 assert!(!h.params.is_empty(), "{verb} must have params");
4168 assert!(
4169 h.params
4170 .iter()
4171 .any(|p| p.name == "profile_id" && p.required),
4172 "{verb} must have required profile_id param"
4173 );
4174 }
4175 }
4176
4177 #[test]
4178 fn brain_reset_params_has_optional_profile_id() {
4179 let h = find_handler("brain.reset");
4180 assert!(!h.params.is_empty(), "brain.reset must have params");
4181 assert!(
4182 h.params
4183 .iter()
4184 .any(|p| p.name == "profile_id" && !p.required),
4185 "brain.reset profile_id must be optional (defaults to balanced-recall-v1)"
4186 );
4187 }
4188
4189 #[test]
4190 fn brain_config_params_has_parameter() {
4191 let h = find_handler("brain.config");
4192 assert!(
4193 !h.params.is_empty(),
4194 "brain.config must document the parameter arg"
4195 );
4196 assert!(
4197 h.params
4198 .iter()
4199 .any(|p| p.name == "parameter" && !p.required),
4200 "brain.config parameter must be optional"
4201 );
4202 }
4203
4204 #[test]
4205 fn brain_events_params_has_limit() {
4206 let h = find_handler("brain.events");
4207 assert!(
4208 !h.params.is_empty(),
4209 "brain.events must document the limit arg"
4210 );
4211 assert!(
4212 h.params.iter().any(|p| p.name == "limit" && !p.required),
4213 "brain.events limit must be optional"
4214 );
4215 }
4216
4217 #[test]
4218 fn brain_emit_params_non_empty_with_target_and_signal() {
4219 let h = find_handler("brain.emit");
4220 assert!(
4221 !h.params.is_empty(),
4222 "brain.emit must have params (mirrors brain.feedback)"
4223 );
4224 assert!(
4225 h.params.iter().any(|p| p.name == "target_id" && p.required),
4226 "brain.emit must have required target_id"
4227 );
4228 assert!(
4229 h.params.iter().any(|p| p.name == "signal" && p.required),
4230 "brain.emit must have required signal"
4231 );
4232 }
4233
4234 #[test]
4235 fn brain_bindings_params_all_optional() {
4236 let h = find_handler("brain.bindings");
4237 assert!(
4238 h.params.iter().all(|p| !p.required),
4239 "brain.bindings: all params must be optional filter args"
4240 );
4241 assert!(
4242 h.params.iter().any(|p| p.name == "profile_id"),
4243 "brain.bindings must document profile_id filter"
4244 );
4245 assert!(
4246 h.params.iter().any(|p| p.name == "consumer_kind"),
4247 "brain.bindings must document consumer_kind filter"
4248 );
4249 }
4250
4251 #[test]
4252 fn brain_create_profile_params_has_required_name() {
4253 let h = find_handler("brain.create_profile");
4254 assert!(
4255 !h.params.is_empty(),
4256 "brain.create_profile must have params"
4257 );
4258 assert!(
4259 h.params.iter().any(|p| p.name == "name" && p.required),
4260 "brain.create_profile must have required name param"
4261 );
4262 assert!(
4263 h.params
4264 .iter()
4265 .any(|p| p.name == "consumer_kind" && !p.required),
4266 "brain.create_profile consumer_kind must be optional"
4267 );
4268 }
4269
4270 fn make_brain_registry() -> (VerbRegistry, KhiveRuntime) {
4285 use khive_pack_kg::KgPack;
4286 use khive_runtime::VerbRegistryBuilder;
4287 let rt = KhiveRuntime::memory().expect("in-memory runtime for brain registry");
4288 let mut builder = VerbRegistryBuilder::new();
4289 builder.register(KgPack::new(rt.clone()));
4290 builder.register(BrainPack::new(rt.clone()));
4291 let registry = builder.build().expect("kg+brain registry builds");
4292 (registry, rt)
4293 }
4294
4295 #[tokio::test]
4298 async fn r2_h1_bind_via_registry_preserves_namespace() {
4299 let (registry, _rt) = make_brain_registry();
4300
4301 let result = registry
4303 .dispatch(
4304 "brain.bind",
4305 json!({
4306 "profile_id": "balanced-recall-v1",
4307 "actor": "alice",
4308 "namespace": "team-a",
4309 "consumer_kind": "recall",
4310 }),
4311 )
4312 .await
4313 .expect("brain.bind must succeed");
4314 assert_eq!(
4315 result["namespace"],
4316 json!("team-a"),
4317 "brain.bind response must echo the caller-supplied namespace"
4318 );
4319
4320 let bindings = registry
4322 .dispatch(
4323 "brain.bindings",
4324 json!({
4325 "profile_id": "balanced-recall-v1",
4326 "namespace": "team-a",
4327 }),
4328 )
4329 .await
4330 .expect("brain.bindings must succeed");
4331 assert_eq!(
4332 bindings["count"],
4333 json!(1u64),
4334 "must find exactly one binding for namespace=team-a"
4335 );
4336 assert_eq!(
4337 bindings["bindings"][0]["namespace"],
4338 json!("team-a"),
4339 "stored binding namespace must be team-a, not wildcard"
4340 );
4341 }
4342
4343 #[tokio::test]
4346 async fn r2_h1_resolve_via_registry_uses_namespace() {
4347 let (registry, _rt) = make_brain_registry();
4348
4349 registry
4351 .dispatch(
4352 "brain.bind",
4353 json!({
4354 "profile_id": "balanced-recall-v1",
4355 "actor": "alice",
4356 "namespace": "team-a",
4357 "consumer_kind": "recall",
4358 }),
4359 )
4360 .await
4361 .expect("brain.bind team-a");
4362
4363 let resolved = registry
4365 .dispatch(
4366 "brain.resolve",
4367 json!({
4368 "actor": "alice",
4369 "namespace": "team-a",
4370 "consumer_kind": "recall",
4371 }),
4372 )
4373 .await
4374 .expect("brain.resolve must succeed for team-a");
4375 assert_eq!(
4376 resolved["resolved_profile_id"],
4377 json!("balanced-recall-v1"),
4378 "resolve must return the profile bound for team-a"
4379 );
4380 }
4381
4382 #[tokio::test]
4385 async fn r2_h1_unbind_via_registry_uses_namespace() {
4386 let (registry, _rt) = make_brain_registry();
4387
4388 registry
4390 .dispatch(
4391 "brain.bind",
4392 json!({
4393 "profile_id": "balanced-recall-v1",
4394 "actor": "alice",
4395 "namespace": "team-a",
4396 "consumer_kind": "recall",
4397 }),
4398 )
4399 .await
4400 .expect("bind team-a");
4401 registry
4402 .dispatch(
4403 "brain.bind",
4404 json!({
4405 "profile_id": "balanced-recall-v1",
4406 "actor": "alice",
4407 "namespace": "team-b",
4408 "consumer_kind": "recall",
4409 }),
4410 )
4411 .await
4412 .expect("bind team-b");
4413
4414 let unbound = registry
4416 .dispatch(
4417 "brain.unbind",
4418 json!({
4419 "actor": "alice",
4420 "namespace": "team-a",
4421 }),
4422 )
4423 .await
4424 .expect("unbind team-a");
4425 assert_eq!(
4426 unbound["unbound"],
4427 json!(1u64),
4428 "must remove exactly one binding (team-a)"
4429 );
4430
4431 let remaining = registry
4433 .dispatch(
4434 "brain.bindings",
4435 json!({
4436 "actor": "alice",
4437 "namespace": "team-b",
4438 }),
4439 )
4440 .await
4441 .expect("bindings after unbind");
4442 assert_eq!(
4443 remaining["count"],
4444 json!(1u64),
4445 "team-b binding must survive the team-a unbind"
4446 );
4447 }
4448}