Skip to main content

cortex_memory/
salience.rs

1//! Deterministic salience brightness scoring.
2
3/// Salience dimensions from BUILD_SPEC ยง6.1.
4///
5/// `Salience` intentionally does not carry memory confidence. Confidence is a
6/// separate epistemic field on memory rows; brightness is retrieval pressure.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct Salience {
9    /// Freshness pressure.
10    pub recency: f32,
11    /// Cross-session recurrence. This is bounded and does not stand in for authority.
12    pub recurrence: f32,
13    /// Usefulness across domains/tasks.
14    pub reusability: f32,
15    /// Validated outcome signal.
16    pub validation: f32,
17    /// Cost or consequence of applying/missing this memory.
18    pub consequence: f32,
19    /// Bounded affective intensity.
20    pub emotional_charge: f32,
21    /// Relevance to stable operator identity/preferences.
22    pub identity_relevance: f32,
23    /// Penalty for contradiction/conflict pressure.
24    pub contradiction_penalty: f32,
25    /// Observed use count. Repeated use without validation becomes weakly negative.
26    pub use_count: u32,
27}
28
29impl Default for Salience {
30    fn default() -> Self {
31        Self {
32            recency: 0.0,
33            recurrence: 0.0,
34            reusability: 0.0,
35            validation: 0.0,
36            consequence: 0.0,
37            emotional_charge: 0.0,
38            identity_relevance: 0.0,
39            contradiction_penalty: 0.0,
40            use_count: 0,
41        }
42    }
43}
44
45/// Compute deterministic brightness in `[0, 1]`.
46///
47/// Brightness is retrieval pressure, not truth confidence. The formula keeps
48/// validation as the strongest positive term and subtracts contradiction
49/// pressure directly.
50#[must_use]
51pub fn brightness(salience: &Salience) -> f32 {
52    let recurrence = bounded(salience.recurrence);
53    let validation = bounded(salience.validation);
54    let unvalidated_use_penalty = if validation <= f32::EPSILON && salience.use_count > 5 {
55        ((salience.use_count - 5) as f32 * 0.01).min(0.10)
56    } else {
57        0.0
58    };
59
60    let score = 0.10
61        + 0.08 * bounded(salience.recency)
62        + 0.14 * bounded(salience.reusability)
63        + 0.30 * validation
64        + 0.14 * bounded(salience.consequence)
65        + 0.06 * bounded(salience.emotional_charge)
66        + 0.12 * bounded(salience.identity_relevance)
67        + 0.06 * recurrence * validation
68        - 0.35 * bounded(salience.contradiction_penalty)
69        - unvalidated_use_penalty;
70
71    bounded(score)
72}
73
74fn bounded(value: f32) -> f32 {
75    if value.is_nan() {
76        return 0.0;
77    }
78    value.clamp(0.0, 1.0)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    fn baseline() -> Salience {
86        Salience {
87            recency: 0.4,
88            recurrence: 0.5,
89            reusability: 0.6,
90            validation: 0.0,
91            consequence: 0.4,
92            emotional_charge: 0.2,
93            identity_relevance: 0.3,
94            contradiction_penalty: 0.0,
95            use_count: 0,
96        }
97    }
98
99    #[test]
100    fn brightness_is_monotonic_in_validation() {
101        let mut previous = 0.0;
102        for step in 0..=20 {
103            let mut salience = baseline();
104            salience.validation = step as f32 / 20.0;
105            let current = brightness(&salience);
106            assert!(
107                current >= previous,
108                "brightness decreased from {previous} to {current} at validation step {step}"
109            );
110            previous = current;
111        }
112    }
113
114    #[test]
115    fn brightness_decreases_with_contradiction_penalty() {
116        let mut previous = 1.0;
117        for step in 0..=20 {
118            let mut salience = baseline();
119            salience.validation = 0.8;
120            salience.contradiction_penalty = step as f32 / 20.0;
121            let current = brightness(&salience);
122            assert!(
123                current <= previous,
124                "brightness increased from {previous} to {current} at penalty step {step}"
125            );
126            previous = current;
127        }
128    }
129
130    #[test]
131    fn repeated_use_without_validation_is_not_positive_uplift() {
132        let mut low_use = baseline();
133        low_use.use_count = 5;
134        let mut repeated = low_use;
135        repeated.use_count = 20;
136
137        assert!(brightness(&repeated) < brightness(&low_use));
138    }
139
140    #[test]
141    fn brightness_is_bounded_and_nan_safe() {
142        let salience = Salience {
143            recency: f32::NAN,
144            recurrence: 99.0,
145            reusability: 99.0,
146            validation: 99.0,
147            consequence: 99.0,
148            emotional_charge: 99.0,
149            identity_relevance: 99.0,
150            contradiction_penalty: -99.0,
151            use_count: 0,
152        };
153
154        let score = brightness(&salience);
155        assert!((0.0..=1.0).contains(&score));
156    }
157}