1use rustc_hash::FxHashMap;
6use rand::{Rng, SeedableRng};
7use rand_chacha::ChaCha8Rng;
8
9#[derive(Debug, Clone, PartialEq)]
12pub struct Hypervector {
13 bits: Vec<u64>,
14 dims: usize,
15}
16
17impl Hypervector {
18 pub fn new(dims: usize) -> Self {
19 let words = dims.div_ceil(64);
20 Self { bits: vec![0u64; words], dims }
21 }
22
23 #[inline(always)]
25 pub fn bits_get(&self, word: usize) -> u64 {
26 *self.bits.get(word).unwrap_or(&0)
27 }
28
29 pub fn get(&self, idx: usize) -> i8 {
30 if idx >= self.dims { return 0; }
31 let word = idx / 64;
32 let bit = idx % 64;
33 if (self.bits[word] >> bit) & 1 == 1 { 1 } else { -1 }
34 }
35
36 #[inline(always)]
38 pub fn set(&mut self, idx: usize, val: i8) {
39 if idx >= self.dims { return; }
40 let word = idx / 64;
41 let bit = idx % 64;
42 if val >= 0 {
43 self.bits[word] |= 1u64 << bit;
44 } else {
45 self.bits[word] &= !(1u64 << bit);
46 }
47 }
48
49 #[inline(always)]
52 pub fn dot(&self, other: &Hypervector) -> i64 {
53 let min_words = self.bits.len().min(other.bits.len());
54 let mut hamming: u64 = 0;
55 for i in 0..min_words {
56 hamming += (self.bits[i] ^ other.bits[i]).count_ones() as u64;
57 }
58 let dims = self.dims.min(other.dims) as i64;
59 dims - 2 * (hamming as i64)
60 }
61
62 #[inline(always)]
65 pub fn norm(&self) -> f64 {
66 (self.dims as f64).sqrt()
67 }
68
69 #[inline(always)]
72 pub fn cosine(&self, other: &Hypervector) -> f64 {
73 let d = self.dot(other) as f64;
74 let n = self.norm() * other.norm();
75 if n == 0.0 { 0.0 } else { d / n }
76 }
77
78 pub fn bundle_into(&mut self, other: &Hypervector) {
81 let words = self.bits.len().min(other.bits.len());
84 let mut acc: Vec<i32> = Vec::with_capacity(64);
85 for w in 0..words {
86 acc.clear();
93 acc.resize(64, 0i32);
94 for (b, slot) in acc.iter_mut().enumerate() {
95 let s = if (self.bits[w] >> b) & 1 == 1 { 1 } else { -1 };
96 let o = if (other.bits[w] >> b) & 1 == 1 { 1 } else { -1 };
97 *slot = s + o;
98 }
99 let mut new_word = 0u64;
102 for (b, slot) in acc.iter().enumerate() {
103 if *slot > 0 || (*slot == 0 && (self.bits[w] >> b) & 1 == 1) {
104 new_word |= 1u64 << b;
105 }
106 }
107 self.bits[w] = new_word;
108 }
109 }
110}
111
112pub fn make_hv(seed: u64, dims: usize) -> Hypervector {
115 let mut rng = ChaCha8Rng::seed_from_u64(seed);
116 let mut hv = Hypervector::new(dims);
117 for i in 0..dims {
118 hv.set(i, if rng.gen_bool(0.5) { 1 } else { -1 });
119 }
120 hv
121}
122
123pub fn bundle(hv: &mut Hypervector, other: &Hypervector) {
125 hv.bundle_into(other);
126}
127
128#[inline(always)]
131pub fn bipolar_clamp(_hv: &mut Hypervector) {
132 }
134
135#[inline(always)]
138pub fn dot(a: &Hypervector, b: &Hypervector) -> f64 {
139 a.cosine(b)
140}
141
142#[derive(Debug, Clone)]
144pub struct MemoryRecord {
145 pub id: i64,
146 pub content: String,
147 pub source: String,
148 pub timestamp: i64,
149 pub tier: i32, pub recall_count: i32,
151 pub error_flag: i32,
152 pub entropy: f64,
153}
154
155#[derive(Debug, Clone)]
157pub struct ScoredMemory {
158 pub memory: MemoryRecord,
159 pub score: f64,
160}
161
162pub fn open_persistent(path: &str) -> Result<MemoryStore, String> {
164 let conn = rusqlite::Connection::open(path).map_err(|e| format!("DB: {}", e))?;
165 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
166 .map_err(|e| format!("PRAGMA: {}", e))?;
167 conn.execute_batch(
168 "CREATE TABLE IF NOT EXISTS cortex_memories (
169 id INTEGER PRIMARY KEY AUTOINCREMENT,
170 content TEXT NOT NULL,
171 source TEXT DEFAULT '',
172 timestamp INTEGER NOT NULL,
173 tier INTEGER DEFAULT 0,
174 recall_count INTEGER DEFAULT 0
175 );"
176 ).map_err(|e| format!("schema: {}", e))?;
177
178 let mut store = MemoryStore::new(100);
179
180 let mut stmt = conn.prepare("SELECT id, content, source, timestamp, tier, recall_count, 0, 0.0 FROM cortex_memories ORDER BY id").map_err(|e| format!("query: {}", e))?;
181 let rows = stmt.query_map([], |row| {
182 Ok(MemoryRecord {
183 id: row.get(0)?,
184 content: row.get(1)?,
185 source: row.get(2)?,
186 timestamp: row.get(3)?,
187 tier: row.get(4)?,
188 recall_count: row.get(5)?,
189 error_flag: row.get(6)?,
190 entropy: row.get(7)?,
191 })
192 }).map_err(|e| format!("rows: {}", e))?;
193
194 for row in rows.flatten() {
195 store.memories.push(row);
196 }
197 Ok(store)
198}
199
200pub fn save_persistent(store: &MemoryStore, path: &str) -> Result<(), String> {
202 let conn = rusqlite::Connection::open(path).map_err(|e| format!("DB: {}", e))?;
203 conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
204 .map_err(|e| format!("PRAGMA: {}", e))?;
205 conn.execute_batch(
206 "CREATE TABLE IF NOT EXISTS cortex_memories (
207 id INTEGER PRIMARY KEY AUTOINCREMENT,
208 content TEXT NOT NULL,
209 source TEXT DEFAULT '',
210 timestamp INTEGER NOT NULL,
211 tier INTEGER DEFAULT 0,
212 recall_count INTEGER DEFAULT 0
213 );"
214 ).map_err(|e| format!("schema: {}", e))?;
215
216 let _ = conn.execute("DELETE FROM cortex_memories", []);
217 for m in &store.memories {
218 if let Err(e) = conn.execute(
219 "INSERT INTO cortex_memories (content, source, timestamp, tier, recall_count) VALUES (?1, ?2, ?3, ?4, ?5)",
220 rusqlite::params![m.content, m.source, m.timestamp, m.tier, m.recall_count],
221 ) {
222 eprintln!("[memory] save: {}", e);
223 }
224 }
225 Ok(())
226}
227
228pub struct MemoryStore {
229 pub memories: Vec<MemoryRecord>,
230 token_hvs: FxHashMap<String, Hypervector>,
231 cooccur: FxHashMap<(String, String), u64>,
232 pub dims: usize,
233}
234
235impl MemoryStore {
236 pub fn new(dims: usize) -> Self {
237 Self { memories: Vec::new(), token_hvs: FxHashMap::default(), cooccur: FxHashMap::default(), dims }
238 }
239
240 pub fn ensure_token_hv(&mut self, token: &str) -> &Hypervector {
242 let dims = self.dims;
243 self.token_hvs.entry(token.to_string())
244 .or_insert_with(|| make_hv(fnv1a(token.as_bytes()), dims))
247 }
248
249 pub fn retain(&mut self, content: &str, source: &str, error: bool) -> i64 {
251 let tokens = scan_tokens(content);
252 if tokens.is_empty() { return 0; }
253
254 for i in 0..tokens.len() {
256 for j in (i + 1)..tokens.len().min(i + 5) {
257 let key = (tokens[i].clone(), tokens[j].clone());
258 *self.cooccur.entry(key).or_insert(0) += 1;
259 self.hebbian_update(&tokens[i], &tokens[j]);
260 }
261 }
262
263 let now = chrono::Utc::now().timestamp();
264 let id = self.memories.len() as i64 + 1;
265 self.memories.push(MemoryRecord {
266 id,
267 content: content.to_string(),
268 source: source.to_string(),
269 timestamp: now,
270 tier: 0,
271 recall_count: 0,
272 error_flag: if error { 1 } else { 0 },
273 entropy: 0.0,
274 });
275 id
276 }
277
278 fn hebbian_update(&mut self, a: &str, b: &str) {
282 let dims = self.dims;
283 let hv_b_val = {
284 let hv_b = self.token_hvs.entry(b.to_string()).or_insert_with(|| make_hv(b.len() as u64, dims));
285 let mut rng = ChaCha8Rng::seed_from_u64(
287 (a.len() as u64).wrapping_mul(31).wrapping_add(b.len() as u64)
288 );
289 let n_flip = (dims / 10).max(1);
290 let mut bits_to_set: Vec<(usize, i8)> = Vec::with_capacity(n_flip);
291 for _ in 0..n_flip {
292 let idx = rng.gen_range(0..dims);
293 bits_to_set.push((idx, hv_b.get(idx)));
294 }
295 bits_to_set
296 };
297 let hv_a = self.token_hvs.entry(a.to_string()).or_insert_with(|| make_hv(a.len() as u64, dims));
299 for (idx, val) in hv_b_val {
300 hv_a.set(idx, val);
301 }
302 }
303
304 pub fn encode_tokens(&mut self, tokens: &[String]) -> Hypervector {
310 let mut sorted: Vec<String> = tokens.to_vec();
311 sorted.sort();
312 sorted.dedup();
313 let k = sorted.len();
314 let dims = self.dims;
315 let hvs: Vec<Hypervector> = sorted.iter()
318 .map(|t| self.ensure_token_hv(t).clone())
319 .collect();
320 let mut hv = Hypervector::new(dims);
321 if k == 0 { return hv; }
322 let ones: Vec<i32> = {
323 let mut acc = vec![0i32; dims];
324 for t in &hvs {
325 for (i, slot) in acc.iter_mut().enumerate() {
326 if t.get(i) == 1 { *slot += 1; }
327 }
328 }
329 acc
330 };
331 for (i, o) in ones.iter().enumerate() {
332 if *o * 2 > k as i32 { hv.set(i, 1); }
333 }
334 hv
335 }
336
337 pub fn encode_query(&self, query: &str) -> Hypervector {
339 let tokens = scan_tokens(query);
340 let mut hv = Hypervector::new(self.dims);
341 for t in tokens {
342 if let Some(thv) = self.token_hvs.get(&t) {
343 hv.bundle_into(thv);
344 }
345 }
346 hv
347 }
348
349 pub fn recall(&self, query: &str, top_n: usize) -> Vec<ScoredMemory> {
352 if self.memories.is_empty() { return vec![]; }
353 let q_hv = self.encode_query(query);
354 let mut scored: Vec<(usize, f64)> = self.memories.iter().enumerate().map(|(i, m)| {
355 let m_hv = self.encode_query(&m.content);
356 let score = q_hv.cosine(&m_hv);
357 (i, score)
358 }).collect();
359 if top_n < scored.len() {
361 scored.select_nth_unstable_by(top_n, |a, b| {
362 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
363 });
364 }
365 scored.truncate(top_n);
366 scored.into_iter().map(|(i, score)| {
367 ScoredMemory { memory: self.memories[i].clone(), score }
368 }).collect()
369 }
370
371 pub fn predict(&self, query: &str, top_n: usize) -> Vec<(String, u64)> {
373 let tokens = scan_tokens(query);
374 let mut scores: FxHashMap<String, u64> = FxHashMap::default();
375 for t in &tokens {
376 for ((a, b), count) in &self.cooccur {
377 if a == t { *scores.entry(b.clone()).or_insert(0) += count; }
378 if b == t { *scores.entry(a.clone()).or_insert(0) += count; }
379 }
380 }
381 let mut sorted: Vec<(String, u64)> = scores.into_iter().collect();
382 if top_n < sorted.len() {
384 sorted.select_nth_unstable_by(top_n, |a, b| b.1.cmp(&a.1));
385 }
386 sorted.truncate(top_n);
387 sorted
388 }
389
390 pub fn consolidate(&mut self) {
392 let now = chrono::Utc::now().timestamp();
393 for m in &mut self.memories {
394 if m.recall_count >= 10 && m.tier < 2 { m.tier = 2; }
395 if m.recall_count >= 3 && m.tier < 1 && now - m.timestamp < 7 * 86400 { m.tier = 1; }
396 }
397 }
398}
399
400pub fn fnv_seed(bytes:&[u8])->u64{fnv1a(bytes)}
403fn fnv1a(bytes: &[u8]) -> u64 {
405 let mut h: u64 = 0xcbf29ce484222325;
406 for &b in bytes {
407 h ^= b as u64;
408 h = h.wrapping_mul(0x100000001b3);
409 }
410 h
411}
412
413pub fn scan_tokens(text: &str) -> Vec<String> {
414 text.split(|c: char| !c.is_alphanumeric() && c != '_')
415 .filter(|t| t.len() >= 3 && t.chars().any(|c| c.is_alphabetic()))
416 .map(|t| t.to_ascii_lowercase())
417 .collect()
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_make_hv() {
426 let hv = make_hv(42, 10000);
427 assert_eq!(hv.dims, 10000);
428 let mut has_pos = false;
430 let mut has_neg = false;
431 for i in 0..100 {
432 match hv.get(i) {
433 1 => has_pos = true,
434 -1 => has_neg = true,
435 _ => {}
436 }
437 }
438 assert!(has_pos, "HV should contain +1 values");
439 assert!(has_neg, "HV should contain -1 values");
440 }
441
442 #[test]
443 fn test_bundle_and_clamp() {
444 let mut a = Hypervector::new(100);
445 let mut b = Hypervector::new(100);
446 for i in 0..100 { a.set(i, 1); b.set(i, -1); }
448 bundle(&mut a, &b);
449 }
454
455 #[test]
456 fn test_retain_and_recall() {
457 let mut store = MemoryStore::new(100);
458 store.retain("Alice works at Google", "user", false);
459 let results = store.recall("Alice", 5);
460 assert_eq!(results.len(), 1);
461 assert!(results[0].score > 0.0);
462 }
463
464 #[test]
465 fn test_prediction() {
466 let mut store = MemoryStore::new(100);
467 store.retain("Alice works at Google", "user", false);
468 store.retain("Bob works at Google too", "user", false);
469 let preds = store.predict("Alice", 5);
470 assert!(!preds.is_empty());
471 assert!(preds.iter().any(|(t, _)| t.contains("google")));
472 }
473
474 #[test]
475 fn test_empty_store() {
476 let store = MemoryStore::new(100);
477 assert!(store.recall("anything", 5).is_empty());
478 }
479}