aria-core 0.1.0

Generic adaptive sequencing engine — zero dependencies, domain-agnostic. Suggest(), feedback(). Works from item one.
Documentation
/// Feedback signal reported by caller after a user interacts with an item.
///
/// Domain-agnostic by design:
///   - In learning:   success = answered correctly, effort = how hard it felt (0=easy, 1=hard)
///   - In ecommerce:  success = purchased/clicked, effort = price relative to budget
///   - In travel:     success = booked/saved, effort = distance/cost relative to preference
///   - In content:    success = completed/liked, effort = complexity relative to level
///
/// Callers may extend context via the `metadata` map for custom StateUpdater logic.
#[derive(Debug, Clone)]
pub struct Signal {
    /// Did the interaction result in a positive outcome?
    pub success: bool,

    /// Normalised effort cost of this interaction. Range [0.0, 1.0].
    /// 0.0 = effortless, 1.0 = maximum effort / friction.
    pub effort: f32,

    /// Optional arbitrary metadata for custom updater logic.
    /// Key-value pairs; values are caller-defined strings.
    pub metadata: std::collections::HashMap<String, String>,
}

impl Signal {
    /// Minimal constructor — no metadata.
    pub fn new(success: bool, effort: f32) -> Self {
        Self {
            success,
            effort: effort.clamp(0.0, 1.0),
            metadata: std::collections::HashMap::new(),
        }
    }

    /// Constructor with metadata.
    pub fn with_metadata(
        success: bool,
        effort: f32,
        metadata: std::collections::HashMap<String, String>,
    ) -> Self {
        Self {
            success,
            effort: effort.clamp(0.0, 1.0),
            metadata,
        }
    }

    /// Composite performance score derived from success + effort.
    /// High success + low effort → score near 1.0 (too easy).
    /// High success + high effort → score near 0.5 (good challenge).
    /// Failure → score near 0.0.
    pub fn performance(&self) -> f32 {
        if self.success {
            0.5 + 0.5 * (1.0 - self.effort)
        } else {
            0.0
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn performance_easy_success() {
        let s = Signal::new(true, 0.0);
        assert!((s.performance() - 1.0).abs() < 1e-6);
    }

    #[test]
    fn performance_hard_success() {
        let s = Signal::new(true, 1.0);
        assert!((s.performance() - 0.5).abs() < 1e-6);
    }

    #[test]
    fn performance_failure() {
        let s = Signal::new(false, 0.3);
        assert!((s.performance() - 0.0).abs() < 1e-6);
    }

    #[test]
    fn effort_clamped() {
        let s = Signal::new(true, 1.5);
        assert_eq!(s.effort, 1.0);
        let s2 = Signal::new(true, -0.5);
        assert_eq!(s2.effort, 0.0);
    }
}