Skip to main content

prosaic_core/
salience.rs

1use crate::context::{Context, Value};
2
3/// Salience level of an event — how much detail/emphasis it deserves.
4///
5/// Templates can be registered for specific salience levels, and the engine
6/// selects the appropriate level based on event magnitude.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub enum Salience {
10    /// Minor changes — single-consumer or no-impact. Brief, often parenthetical.
11    Low,
12    /// Standard changes — the default verbosity level.
13    #[default]
14    Medium,
15    /// Major changes — high impact, deserves elaboration.
16    High,
17}
18
19/// Thresholds for automatic salience derivation from context.
20#[derive(Debug, Clone, Copy)]
21pub struct SalienceThresholds {
22    /// consumer_count below this is Low (exclusive).
23    pub low_max: i64,
24    /// consumer_count at or above this is High.
25    pub high_min: i64,
26}
27
28impl Default for SalienceThresholds {
29    fn default() -> Self {
30        Self {
31            low_max: 2,   // 0, 1 → Low
32            high_min: 20, // 20+ → High; 2-19 → Medium
33        }
34    }
35}
36
37impl Salience {
38    /// Derive salience from a rendering context.
39    ///
40    /// Order of precedence:
41    /// 1. Explicit `salience` key in context (with value "low"/"medium"/"high")
42    /// 2. `consumer_count` mapped through thresholds
43    /// 3. Default (Medium)
44    pub fn from_context(ctx: &Context, thresholds: SalienceThresholds) -> Self {
45        // Explicit override
46        if let Some(Value::String(s)) = ctx.get("salience") {
47            match s.to_lowercase().as_str() {
48                "low" => return Salience::Low,
49                "medium" => return Salience::Medium,
50                "high" => return Salience::High,
51                _ => {}
52            }
53        }
54
55        // Derive from consumer_count
56        if let Some(Value::Number(n)) = ctx.get("consumer_count") {
57            if *n < thresholds.low_max {
58                return Salience::Low;
59            }
60            if *n >= thresholds.high_min {
61                return Salience::High;
62            }
63            return Salience::Medium;
64        }
65
66        Salience::default()
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    fn ctx_with(key: &str, value: Value) -> Context {
75        let mut c = Context::new();
76        c.insert(key, value);
77        c
78    }
79
80    #[test]
81    fn explicit_salience_overrides_count() {
82        let mut c = Context::new();
83        c.insert("consumer_count", Value::Number(100));
84        c.insert("salience", Value::String("low".into()));
85
86        assert_eq!(
87            Salience::from_context(&c, SalienceThresholds::default()),
88            Salience::Low
89        );
90    }
91
92    #[test]
93    fn derives_low_from_small_count() {
94        let c = ctx_with("consumer_count", Value::Number(1));
95        assert_eq!(
96            Salience::from_context(&c, SalienceThresholds::default()),
97            Salience::Low
98        );
99    }
100
101    #[test]
102    fn derives_high_from_large_count() {
103        let c = ctx_with("consumer_count", Value::Number(25));
104        assert_eq!(
105            Salience::from_context(&c, SalienceThresholds::default()),
106            Salience::High
107        );
108    }
109
110    #[test]
111    fn derives_medium_from_middle_count() {
112        let c = ctx_with("consumer_count", Value::Number(5));
113        assert_eq!(
114            Salience::from_context(&c, SalienceThresholds::default()),
115            Salience::Medium
116        );
117    }
118
119    #[test]
120    fn defaults_to_medium_without_count() {
121        let c = Context::new();
122        assert_eq!(
123            Salience::from_context(&c, SalienceThresholds::default()),
124            Salience::Medium
125        );
126    }
127
128    #[test]
129    fn zero_count_is_low() {
130        let c = ctx_with("consumer_count", Value::Number(0));
131        assert_eq!(
132            Salience::from_context(&c, SalienceThresholds::default()),
133            Salience::Low
134        );
135    }
136
137    #[test]
138    fn unknown_salience_string_falls_back() {
139        let mut c = Context::new();
140        c.insert("consumer_count", Value::Number(5));
141        c.insert("salience", Value::String("bogus".into()));
142        assert_eq!(
143            Salience::from_context(&c, SalienceThresholds::default()),
144            Salience::Medium
145        );
146    }
147}