aria_core/signal.rs
1/// Feedback signal reported by caller after a user interacts with an item.
2///
3/// Domain-agnostic by design:
4/// - In learning: success = answered correctly, effort = how hard it felt (0=easy, 1=hard)
5/// - In ecommerce: success = purchased/clicked, effort = price relative to budget
6/// - In travel: success = booked/saved, effort = distance/cost relative to preference
7/// - In content: success = completed/liked, effort = complexity relative to level
8///
9/// Callers may extend context via the `metadata` map for custom StateUpdater logic.
10#[derive(Debug, Clone)]
11pub struct Signal {
12 /// Did the interaction result in a positive outcome?
13 pub success: bool,
14
15 /// Normalised effort cost of this interaction. Range [0.0, 1.0].
16 /// 0.0 = effortless, 1.0 = maximum effort / friction.
17 pub effort: f32,
18
19 /// Optional arbitrary metadata for custom updater logic.
20 /// Key-value pairs; values are caller-defined strings.
21 pub metadata: std::collections::HashMap<String, String>,
22}
23
24impl Signal {
25 /// Minimal constructor — no metadata.
26 pub fn new(success: bool, effort: f32) -> Self {
27 Self {
28 success,
29 effort: effort.clamp(0.0, 1.0),
30 metadata: std::collections::HashMap::new(),
31 }
32 }
33
34 /// Constructor with metadata.
35 pub fn with_metadata(
36 success: bool,
37 effort: f32,
38 metadata: std::collections::HashMap<String, String>,
39 ) -> Self {
40 Self {
41 success,
42 effort: effort.clamp(0.0, 1.0),
43 metadata,
44 }
45 }
46
47 /// Composite performance score derived from success + effort.
48 /// High success + low effort → score near 1.0 (too easy).
49 /// High success + high effort → score near 0.5 (good challenge).
50 /// Failure → score near 0.0.
51 pub fn performance(&self) -> f32 {
52 if self.success {
53 0.5 + 0.5 * (1.0 - self.effort)
54 } else {
55 0.0
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn performance_easy_success() {
66 let s = Signal::new(true, 0.0);
67 assert!((s.performance() - 1.0).abs() < 1e-6);
68 }
69
70 #[test]
71 fn performance_hard_success() {
72 let s = Signal::new(true, 1.0);
73 assert!((s.performance() - 0.5).abs() < 1e-6);
74 }
75
76 #[test]
77 fn performance_failure() {
78 let s = Signal::new(false, 0.3);
79 assert!((s.performance() - 0.0).abs() < 1e-6);
80 }
81
82 #[test]
83 fn effort_clamped() {
84 let s = Signal::new(true, 1.5);
85 assert_eq!(s.effort, 1.0);
86 let s2 = Signal::new(true, -0.5);
87 assert_eq!(s2.effort, 0.0);
88 }
89}