Skip to main content

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}