Skip to main content

microscope_memory/
salience.rs

1//! Salience Network — kiemelési hálózat a belső narratíva előtt.
2//!
3//! Az emberi agy salience network-je eldönti, hogy a milliónyi
4//! háttérfolyamatból mi kerüljön a tudatos belső beszédbe.
5//!
6//! Itt ugyanez: minden recall path kap egy salience pontszámot
7//! (érzelmi elmozdulás × insight score × recency / inhibition),
8//! és csak a legmagasabb prioritású hullám kerül a narratívába.
9//!
10//! Binary format: SAL1
11//!   magic: "SAL1" (4 bytes)
12//!   count: u32 (inhibition entries)
13//!   entries[]:
14//!     topic_hash: u64 | remaining_strength: f32 | created_ms: u64 (20 bytes)
15
16use std::fs;
17use std::path::Path;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20// ─── Constants ──────────────────────────────────────
21
22/// Alap salience threshold (0-1). Ez alatt nem kerül a narratívába.
23const SALIENCE_THRESHOLD: f32 = 0.3;
24/// Inhibíció erőssége: mennyivel csökken egy téma salience-je ha
25/// nemrég volt a narratívában.
26const INHIBITION_STRENGTH: f32 = 0.5;
27/// Inhibíció felezési ideje (ms). 5 perc alatt feleződik.
28const INHIBITION_HALFLIFE_MS: u64 = 300_000;
29/// Max inhibíciós entry-k száma (LRU evict).
30const MAX_INHIBITIONS: usize = 50;
31
32// ─── Inhibition Entry ──────────────────────────────
33
34/// Egy legátolt téma: a rendszer "nem akar róla beszélni" egy ideig.
35#[derive(Clone, Debug)]
36pub struct InhibitionEntry {
37    pub topic_hash: u64,
38    pub remaining_strength: f32,
39    pub created_ms: u64,
40}
41
42// ─── SalienceState ─────────────────────────────────
43
44/// A salience network állapota: inhibíciós maszk + szűrés.
45pub struct SalienceState {
46    pub inhibitions: Vec<InhibitionEntry>,
47}
48
49impl SalienceState {
50    /// Betöltés SAL1 file-ból, vagy üres init.
51    pub fn load_or_init(output_dir: &Path) -> Self {
52        let path = output_dir.join("salience.bin");
53        if let Ok(data) = fs::read(&path) {
54            if data.len() >= 8 && &data[0..4] == b"SAL1" {
55                let count = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
56                let mut inhibitions = Vec::with_capacity(count);
57                let mut off = 8;
58                for _ in 0..count {
59                    if off + 20 > data.len() {
60                        break;
61                    }
62                    let topic_hash = u64::from_le_bytes(data[off..off + 8].try_into().unwrap());
63                    let remaining = f32::from_le_bytes(data[off + 8..off + 12].try_into().unwrap());
64                    let created = u64::from_le_bytes(data[off + 12..off + 20].try_into().unwrap());
65                    inhibitions.push(InhibitionEntry {
66                        topic_hash,
67                        remaining_strength: remaining,
68                        created_ms: created,
69                    });
70                    off += 20;
71                }
72                return SalienceState { inhibitions };
73            }
74        }
75        SalienceState {
76            inhibitions: Vec::new(),
77        }
78    }
79
80    /// Mentés SAL1 formátumba.
81    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
82        let path = output_dir.join("salience.bin");
83        let mut buf = Vec::with_capacity(8 + self.inhibitions.len() * 20);
84        buf.extend_from_slice(b"SAL1");
85        buf.extend_from_slice(&(self.inhibitions.len() as u32).to_le_bytes());
86        for e in &self.inhibitions {
87            buf.extend_from_slice(&e.topic_hash.to_le_bytes());
88            buf.extend_from_slice(&e.remaining_strength.to_le_bytes());
89            buf.extend_from_slice(&e.created_ms.to_le_bytes());
90        }
91        let tmp_path = output_dir.join("salience.bin.tmp");
92        fs::write(&tmp_path, &buf).map_err(|e| format!("write salience.bin: {}", e))?;
93        fs::rename(&tmp_path, &path).map_err(|e| format!("rename salience.bin: {}", e))
94    }
95
96    /// Decay inhibíciók: csökkenti a remaining_strength-et idő alapján.
97    pub fn decay(&mut self) {
98        let now = now_ms();
99        let half_life = INHIBITION_HALFLIFE_MS as f32;
100        self.inhibitions.retain(|e| {
101            let age = now.saturating_sub(e.created_ms) as f32;
102            let decay = (-age / half_life).exp();
103            let remaining = e.remaining_strength * decay;
104            remaining > 0.01 // prune if negligible
105        });
106    }
107
108    /// Inhibíció hozzáadása egy témához (topic_hash).
109    pub fn inhibit(&mut self, topic_hash: u64) {
110        // Ha már van, erősítsd
111        if let Some(existing) = self
112            .inhibitions
113            .iter_mut()
114            .find(|e| e.topic_hash == topic_hash)
115        {
116            existing.remaining_strength =
117                (existing.remaining_strength + INHIBITION_STRENGTH).min(1.0);
118            existing.created_ms = now_ms();
119            return;
120        }
121        // Különben add hozzá
122        if self.inhibitions.len() >= MAX_INHIBITIONS {
123            self.inhibitions.remove(0); // LRU
124        }
125        self.inhibitions.push(InhibitionEntry {
126            topic_hash,
127            remaining_strength: INHIBITION_STRENGTH,
128            created_ms: now_ms(),
129        });
130    }
131
132    /// Salience score kiszámítása adott blokkra.
133    /// Alap = emotional_delta × insight_score × recency_factor
134    /// Ezt csökkenti a téma inhibíciója.
135    pub fn compute_salience(
136        &self,
137        emotional_delta: f32,
138        insight_score: f32,
139        recency_factor: f32,
140        topic_hash: u64,
141    ) -> f32 {
142        let base = emotional_delta * insight_score * recency_factor;
143        // Inhibíció: ha a téma nemrég volt a narratívában, csökkent
144        let inhibition = self
145            .inhibitions
146            .iter()
147            .find(|e| e.topic_hash == topic_hash)
148            .map(|e| e.remaining_strength)
149            .unwrap_or(0.0);
150        let salience = base * (1.0 - inhibition);
151        if salience < SALIENCE_THRESHOLD {
152            0.0
153        } else {
154            salience
155        }
156    }
157
158    /// Kompakt hash egy szövegből (használható topic_hash-ként).
159    pub fn topic_hash(text: &str) -> u64 {
160        let bytes = text.as_bytes();
161        let mut h = 0x9e3779b97f4a7c15u64;
162        for chunk in bytes.chunks(8) {
163            let mut word = 0u64;
164            for (i, &b) in chunk.iter().enumerate() {
165                word |= (b as u64) << (i * 8);
166            }
167            h = h.wrapping_mul(0x517cc1b727220a95).wrapping_add(word);
168        }
169        h
170    }
171
172    /// Szűrés: visszaadja a salience score-okat minden blokkra.
173    /// Csak a threshold felettieket. Előtte decay-t hív (self->&mut).
174    pub fn filter(
175        &mut self,
176        scores: &[(u32, f32, f32, f32)], // (block_idx, emotional_delta, insight_score, recency)
177    ) -> Vec<(u32, f32)> {
178        self.decay();
179        scores
180            .iter()
181            .filter_map(|&(idx, ed, isc, rec)| {
182                let topic_h = SalienceState::topic_hash(&format!("block_{}", idx));
183                let s = self.compute_salience(ed, isc, rec, topic_h);
184                if s > SALIENCE_THRESHOLD {
185                    Some((idx, s))
186                } else {
187                    None
188                }
189            })
190            .collect()
191    }
192}
193
194fn now_ms() -> u64 {
195    SystemTime::now()
196        .duration_since(UNIX_EPOCH)
197        .unwrap_or_default()
198        .as_millis() as u64
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_empty_state() {
207        let s = SalienceState::load_or_init(Path::new("."));
208        assert!(s.inhibitions.is_empty());
209    }
210
211    #[test]
212    fn test_inhibit_and_decay() {
213        let mut s = SalienceState::load_or_init(Path::new("."));
214        s.inhibit(42);
215        assert_eq!(s.inhibitions.len(), 1);
216        assert!((s.inhibitions[0].remaining_strength - 0.5).abs() < 0.01);
217        s.decay();
218        let score = s.compute_salience(1.0, 0.8, 1.0, 42);
219        assert!(score < 0.5); // should be reduced by inhibition
220    }
221
222    #[test]
223    fn test_save_load_roundtrip() {
224        let dir = std::env::temp_dir();
225        let mut s = SalienceState::load_or_init(&dir);
226        s.inhibit(100);
227        s.inhibit(200);
228        s.save(&dir).unwrap();
229
230        let loaded = SalienceState::load_or_init(&dir);
231        assert_eq!(loaded.inhibitions.len(), 2);
232
233        let _ = std::fs::remove_file(dir.join("salience.bin"));
234    }
235
236    #[test]
237    fn test_topic_hash_deterministic() {
238        let h1 = SalienceState::topic_hash("hello world");
239        let h2 = SalienceState::topic_hash("hello world");
240        assert_eq!(h1, h2);
241    }
242
243    #[test]
244    fn test_filter_below_threshold() {
245        let mut s = SalienceState::load_or_init(Path::new("."));
246        let scores = vec![(0u32, 0.01f32, 0.01, 0.01)]; // very low everything
247        let filtered = s.filter(&scores);
248        assert!(filtered.is_empty());
249    }
250
251    #[test]
252    fn test_inhibition_reduces_salience() {
253        let mut s = SalienceState::load_or_init(Path::new("."));
254        let topic = SalienceState::topic_hash("test_topic");
255
256        // Before inhibition
257        let before = s.compute_salience(1.0, 0.8, 1.0, topic);
258
259        // After inhibition
260        s.inhibit(topic);
261        let after = s.compute_salience(1.0, 0.8, 1.0, topic);
262
263        assert!(after < before);
264    }
265}