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