crtx-memory 0.1.1

Memory lifecycle, salience, decay policies, and contradiction objects.
Documentation
//! Deterministic salience brightness scoring.

/// Salience dimensions from BUILD_SPEC ยง6.1.
///
/// `Salience` intentionally does not carry memory confidence. Confidence is a
/// separate epistemic field on memory rows; brightness is retrieval pressure.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Salience {
    /// Freshness pressure.
    pub recency: f32,
    /// Cross-session recurrence. This is bounded and does not stand in for authority.
    pub recurrence: f32,
    /// Usefulness across domains/tasks.
    pub reusability: f32,
    /// Validated outcome signal.
    pub validation: f32,
    /// Cost or consequence of applying/missing this memory.
    pub consequence: f32,
    /// Bounded affective intensity.
    pub emotional_charge: f32,
    /// Relevance to stable operator identity/preferences.
    pub identity_relevance: f32,
    /// Penalty for contradiction/conflict pressure.
    pub contradiction_penalty: f32,
    /// Observed use count. Repeated use without validation becomes weakly negative.
    pub use_count: u32,
}

impl Default for Salience {
    fn default() -> Self {
        Self {
            recency: 0.0,
            recurrence: 0.0,
            reusability: 0.0,
            validation: 0.0,
            consequence: 0.0,
            emotional_charge: 0.0,
            identity_relevance: 0.0,
            contradiction_penalty: 0.0,
            use_count: 0,
        }
    }
}

/// Compute deterministic brightness in `[0, 1]`.
///
/// Brightness is retrieval pressure, not truth confidence. The formula keeps
/// validation as the strongest positive term and subtracts contradiction
/// pressure directly.
#[must_use]
pub fn brightness(salience: &Salience) -> f32 {
    let recurrence = bounded(salience.recurrence);
    let validation = bounded(salience.validation);
    let unvalidated_use_penalty = if validation <= f32::EPSILON && salience.use_count > 5 {
        ((salience.use_count - 5) as f32 * 0.01).min(0.10)
    } else {
        0.0
    };

    let score = 0.10
        + 0.08 * bounded(salience.recency)
        + 0.14 * bounded(salience.reusability)
        + 0.30 * validation
        + 0.14 * bounded(salience.consequence)
        + 0.06 * bounded(salience.emotional_charge)
        + 0.12 * bounded(salience.identity_relevance)
        + 0.06 * recurrence * validation
        - 0.35 * bounded(salience.contradiction_penalty)
        - unvalidated_use_penalty;

    bounded(score)
}

fn bounded(value: f32) -> f32 {
    if value.is_nan() {
        return 0.0;
    }
    value.clamp(0.0, 1.0)
}

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

    fn baseline() -> Salience {
        Salience {
            recency: 0.4,
            recurrence: 0.5,
            reusability: 0.6,
            validation: 0.0,
            consequence: 0.4,
            emotional_charge: 0.2,
            identity_relevance: 0.3,
            contradiction_penalty: 0.0,
            use_count: 0,
        }
    }

    #[test]
    fn brightness_is_monotonic_in_validation() {
        let mut previous = 0.0;
        for step in 0..=20 {
            let mut salience = baseline();
            salience.validation = step as f32 / 20.0;
            let current = brightness(&salience);
            assert!(
                current >= previous,
                "brightness decreased from {previous} to {current} at validation step {step}"
            );
            previous = current;
        }
    }

    #[test]
    fn brightness_decreases_with_contradiction_penalty() {
        let mut previous = 1.0;
        for step in 0..=20 {
            let mut salience = baseline();
            salience.validation = 0.8;
            salience.contradiction_penalty = step as f32 / 20.0;
            let current = brightness(&salience);
            assert!(
                current <= previous,
                "brightness increased from {previous} to {current} at penalty step {step}"
            );
            previous = current;
        }
    }

    #[test]
    fn repeated_use_without_validation_is_not_positive_uplift() {
        let mut low_use = baseline();
        low_use.use_count = 5;
        let mut repeated = low_use;
        repeated.use_count = 20;

        assert!(brightness(&repeated) < brightness(&low_use));
    }

    #[test]
    fn brightness_is_bounded_and_nan_safe() {
        let salience = Salience {
            recency: f32::NAN,
            recurrence: 99.0,
            reusability: 99.0,
            validation: 99.0,
            consequence: 99.0,
            emotional_charge: 99.0,
            identity_relevance: 99.0,
            contradiction_penalty: -99.0,
            use_count: 0,
        };

        let score = brightness(&salience);
        assert!((0.0..=1.0).contains(&score));
    }
}