1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use khive_storage::event::Event;
5use khive_types::EventOutcome;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum FeedbackSignal {
11 Useful,
12 NotUseful,
13 Wrong,
14}
15
16#[derive(Debug)]
18pub enum BrainSignal {
19 RecallHit { target_id: Uuid, latency_us: i64 },
21 RecallMiss,
23 SearchCompleted { latency_us: i64 },
25 Feedback {
27 target_id: Uuid,
28 signal: FeedbackSignal,
29 },
30 NoteAccessed { target_id: Uuid },
32 Irrelevant,
34}
35
36pub fn interpret(event: &Event) -> BrainSignal {
41 match event.verb.as_str() {
42 "recall" => match event.outcome {
43 EventOutcome::Success => match event.target_id {
44 Some(tid) => BrainSignal::RecallHit {
45 target_id: tid,
46 latency_us: event.duration_us,
47 },
48 None => BrainSignal::RecallMiss,
49 },
50 _ => BrainSignal::RecallMiss,
51 },
52 "search" => BrainSignal::SearchCompleted {
53 latency_us: event.duration_us,
54 },
55 "brain.emit" => {
56 let target = match event.target_id {
57 Some(t) => t,
58 None => return BrainSignal::Irrelevant,
59 };
60 let signal = event
61 .data
62 .as_ref()
63 .and_then(|d| d.get("signal"))
64 .and_then(|s| serde_json::from_value::<FeedbackSignal>(s.clone()).ok());
65 match signal {
66 Some(s) => BrainSignal::Feedback {
67 target_id: target,
68 signal: s,
69 },
70 None => BrainSignal::Irrelevant,
71 }
72 }
73 "get" | "remember" => match event.target_id {
74 Some(tid) => BrainSignal::NoteAccessed { target_id: tid },
75 None => BrainSignal::Irrelevant,
76 },
77 _ => BrainSignal::Irrelevant,
78 }
79}
80
81pub fn entity_signal(signal: &BrainSignal) -> Option<(Uuid, bool)> {
83 match signal {
84 BrainSignal::RecallHit { target_id, .. } => Some((*target_id, true)),
85 BrainSignal::NoteAccessed { target_id } => Some((*target_id, true)),
86 BrainSignal::Feedback {
87 target_id, signal, ..
88 } => Some((*target_id, matches!(signal, FeedbackSignal::Useful))),
89 BrainSignal::RecallMiss | BrainSignal::SearchCompleted { .. } | BrainSignal::Irrelevant => {
90 None
91 }
92 }
93}
94
95pub fn is_recall_positive(signal: &BrainSignal) -> Option<bool> {
97 match signal {
98 BrainSignal::RecallHit { .. } => Some(true),
99 BrainSignal::RecallMiss => Some(false),
100 _ => None,
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use khive_types::SubstrateKind;
108
109 fn make_event(verb: &str, outcome: EventOutcome, target: Option<Uuid>) -> Event {
110 let mut e = Event::new("test", verb, SubstrateKind::Note, "brain");
111 e.outcome = outcome;
112 e.target_id = target;
113 e
114 }
115
116 #[test]
117 fn recall_success_with_target_is_hit() {
118 let id = Uuid::new_v4();
119 let e = make_event("recall", EventOutcome::Success, Some(id));
120 match interpret(&e) {
121 BrainSignal::RecallHit { target_id, .. } => assert_eq!(target_id, id),
122 other => panic!("expected RecallHit, got {other:?}"),
123 }
124 }
125
126 #[test]
127 fn recall_success_without_target_is_miss() {
128 let e = make_event("recall", EventOutcome::Success, None);
129 assert!(matches!(interpret(&e), BrainSignal::RecallMiss));
130 }
131
132 #[test]
133 fn recall_error_is_miss() {
134 let e = make_event("recall", EventOutcome::Error, Some(Uuid::new_v4()));
135 assert!(matches!(interpret(&e), BrainSignal::RecallMiss));
136 }
137
138 #[test]
139 fn search_is_completed() {
140 let e = make_event("search", EventOutcome::Success, None);
141 assert!(matches!(interpret(&e), BrainSignal::SearchCompleted { .. }));
142 }
143
144 #[test]
145 fn brain_emit_with_feedback() {
146 let id = Uuid::new_v4();
147 let mut e = make_event("brain.emit", EventOutcome::Success, Some(id));
148 e.data = Some(serde_json::json!({"signal": "useful"}));
149 match interpret(&e) {
150 BrainSignal::Feedback { target_id, signal } => {
151 assert_eq!(target_id, id);
152 assert_eq!(signal, FeedbackSignal::Useful);
153 }
154 other => panic!("expected Feedback, got {other:?}"),
155 }
156 }
157
158 #[test]
159 fn brain_emit_without_target_is_irrelevant() {
160 let e = make_event("brain.emit", EventOutcome::Success, None);
161 assert!(matches!(interpret(&e), BrainSignal::Irrelevant));
162 }
163
164 #[test]
165 fn unknown_verb_is_irrelevant() {
166 let e = make_event("link", EventOutcome::Success, Some(Uuid::new_v4()));
167 assert!(matches!(interpret(&e), BrainSignal::Irrelevant));
168 }
169
170 #[test]
171 fn entity_signal_for_hit() {
172 let id = Uuid::new_v4();
173 let sig = BrainSignal::RecallHit {
174 target_id: id,
175 latency_us: 100,
176 };
177 assert_eq!(entity_signal(&sig), Some((id, true)));
178 }
179
180 #[test]
181 fn entity_signal_for_miss() {
182 assert_eq!(entity_signal(&BrainSignal::RecallMiss), None);
183 }
184
185 #[test]
186 fn recall_positive_classification() {
187 let hit = BrainSignal::RecallHit {
188 target_id: Uuid::new_v4(),
189 latency_us: 0,
190 };
191 assert_eq!(is_recall_positive(&hit), Some(true));
192 assert_eq!(is_recall_positive(&BrainSignal::RecallMiss), Some(false));
193 assert_eq!(
194 is_recall_positive(&BrainSignal::SearchCompleted { latency_us: 0 }),
195 None
196 );
197 }
198
199 #[test]
200 fn feedback_not_useful_is_negative_entity_signal() {
201 let id = Uuid::new_v4();
202 let sig = BrainSignal::Feedback {
203 target_id: id,
204 signal: FeedbackSignal::NotUseful,
205 };
206 assert_eq!(entity_signal(&sig), Some((id, false)));
207 }
208
209 #[test]
210 fn feedback_wrong_is_negative_entity_signal() {
211 let id = Uuid::new_v4();
212 let sig = BrainSignal::Feedback {
213 target_id: id,
214 signal: FeedbackSignal::Wrong,
215 };
216 assert_eq!(entity_signal(&sig), Some((id, false)));
217 }
218
219 #[test]
220 fn brain_emit_invalid_signal_data_is_irrelevant() {
221 let id = Uuid::new_v4();
222 let mut e = make_event("brain.emit", EventOutcome::Success, Some(id));
223 e.data = Some(serde_json::json!({"signal": "bad_value"}));
224 assert!(matches!(interpret(&e), BrainSignal::Irrelevant));
225 }
226
227 #[test]
228 fn note_accessed_via_get_verb_is_positive_entity_signal() {
229 let id = Uuid::new_v4();
230 let e = make_event("get", EventOutcome::Success, Some(id));
231 match interpret(&e) {
232 BrainSignal::NoteAccessed { target_id } => {
233 assert_eq!(target_id, id);
234 assert_eq!(
235 entity_signal(&BrainSignal::NoteAccessed { target_id }),
236 Some((id, true))
237 );
238 }
239 other => panic!("expected NoteAccessed, got {other:?}"),
240 }
241 }
242
243 #[test]
244 fn note_accessed_via_remember_verb_is_positive_entity_signal() {
245 let id = Uuid::new_v4();
246 let e = make_event("remember", EventOutcome::Success, Some(id));
247 match interpret(&e) {
248 BrainSignal::NoteAccessed { target_id } => {
249 assert_eq!(target_id, id);
250 }
251 other => panic!("expected NoteAccessed, got {other:?}"),
252 }
253 }
254}