1use std::collections::HashMap;
2
3pub use crate::entry::Reinforcement;
8
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
12pub struct Drawer {
13 pub id: String, pub content: String, pub wing: String, pub room: String, pub source_file: String,
18 pub chunk_index: u32,
19 pub filed_at: chrono::DateTime<chrono::Utc>,
20 #[serde(default = "default_confidence")]
23 pub confidence: f32,
24 #[serde(default)]
26 pub access_count: u32,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub accessed_at: Option<chrono::DateTime<chrono::Utc>>,
30 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub linked_ids: Vec<String>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub superseded_by: Option<String>,
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub reinforcements: Vec<Reinforcement>,
39 #[serde(default = "default_active")]
41 pub active: bool,
42}
43
44fn default_confidence() -> f32 { 0.7 }
45fn default_active() -> bool { true }
46
47impl Drawer {
48 pub fn new(
50 id: String,
51 content: String,
52 wing: String,
53 room: String,
54 source_file: String,
55 chunk_index: u32,
56 ) -> Self {
57 Self {
58 id,
59 content,
60 wing,
61 room,
62 source_file,
63 chunk_index,
64 filed_at: chrono::Utc::now(),
65 confidence: 0.7,
66 access_count: 0,
67 accessed_at: None,
68 linked_ids: Vec::new(),
69 superseded_by: None,
70 reinforcements: Vec::new(),
71 active: true,
72 }
73 }
74
75 pub fn effective_confidence(&self) -> f32 {
84 let base = self.confidence;
85
86 let access_boost = if self.access_count > 1 {
88 (self.access_count as f32).ln() * 0.05
89 } else {
90 0.0
91 };
92
93 let age_days = self.accessed_at
95 .or(Some(self.filed_at))
96 .map(|t| chrono::Utc::now().signed_duration_since(t).num_days().max(0) as f32)
97 .unwrap_or(0.0);
98 let age_decay = (age_days / 365.0).min(0.3);
99
100 let reinforcement_adj = if self.reinforcements.is_empty() {
102 0.0
103 } else {
104 let recent: Vec<f32> = self.reinforcements.iter()
105 .rev()
106 .take(10)
107 .map(|r| r.score)
108 .collect();
109 let avg: f32 = recent.iter().sum::<f32>() / recent.len() as f32;
110 (avg * 0.2).clamp(-0.15, 0.15)
111 };
112
113 (base + access_boost - age_decay + reinforcement_adj).clamp(0.0, 1.0)
114 }
115
116 pub fn touch(&mut self) {
118 self.access_count += 1;
119 self.accessed_at = Some(chrono::Utc::now());
120 }
121
122 pub fn reinforce(&mut self, score: f32, context: impl Into<String>) {
124 self.reinforcements.push(Reinforcement {
125 timestamp: chrono::Utc::now(),
126 score: score.clamp(-1.0, 1.0),
127 context: context.into(),
128 related_to: None,
129 });
130 }
131
132 pub fn decay_confidence(&mut self, amount: f32) {
134 self.confidence = (self.confidence - amount).max(0.0);
135 }
136
137 pub fn supersede(&mut self, new_id: &str) {
139 self.superseded_by = Some(new_id.to_string());
140 self.active = false;
141 }
142
143 pub fn link_to(&mut self, other_id: &str) {
145 if !self.linked_ids.iter().any(|id| id == other_id) {
146 self.linked_ids.push(other_id.to_string());
147 }
148 }
149}
150
151#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
153pub struct Closet {
154 pub topic: String,
155 pub entities: Vec<String>,
156 pub drawer_ids: Vec<String>,
157}
158
159#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
161pub struct Tunnel {
162 pub source_wing: String,
163 pub source_room: String,
164 pub target_wing: String,
165 pub target_room: String,
166 pub relation: String, }
168
169impl Tunnel {
170 pub fn source_scope(&self) -> String {
172 format!("{}/{}", self.source_wing, self.source_room)
173 }
174
175 pub fn target_scope(&self) -> String {
177 format!("{}/{}", self.target_wing, self.target_room)
178 }
179}
180
181#[derive(Debug, Default)]
182pub struct Room {
183 pub drawers: Vec<Drawer>,
184 pub closets: Vec<Closet>,
185}
186
187#[derive(Debug, Default)]
188pub struct Wing {
189 pub rooms: HashMap<String, Room>,
190 pub halls: Vec<String>, }
192
193#[derive(Debug, Default)]
195pub struct MemoryTaxonomy {
196 pub wings: HashMap<String, Wing>,
197 pub tunnels: Vec<Tunnel>,
198}
199
200impl MemoryTaxonomy {
201 pub fn new() -> Self {
203 Self::default()
204 }
205
206 pub fn ingest(&mut self, wing: &str, room: &str, source_file: &str, text: &str) {
208 const CHUNK_SIZE: usize = 800;
209 const OVERLAP: usize = 100;
210
211 let wing_entry = self.wings.entry(wing.to_string()).or_default();
212 let room_entry = wing_entry.rooms.entry(room.to_string()).or_default();
213
214 let chars: Vec<char> = text.chars().collect();
215 let mut start = 0usize;
216 let mut chunk_index = 0u32;
217
218 while start < chars.len() {
219 let end = (start + CHUNK_SIZE).min(chars.len());
220 let chunk: String = chars[start..end].iter().collect();
221 let hash = crate::cas::ContentAddressedStorage::hash(&chunk);
222 let id = format!("drawer_{}_{}_{}", wing, room, &hash[..8]);
223
224 room_entry.drawers.push(Drawer::new(
225 id, chunk, wing.to_string(), room.to_string(),
226 source_file.to_string(), chunk_index,
227 ));
228
229 if end == chars.len() {
230 break;
231 }
232 start = end.saturating_sub(OVERLAP);
233 chunk_index += 1;
234 }
235 }
236
237 pub fn add_closet(&mut self, wing: &str, room: &str, closet: Closet) {
239 let wing_entry = self.wings.entry(wing.to_string()).or_default();
240 let room_entry = wing_entry.rooms.entry(room.to_string()).or_default();
241 room_entry.closets.push(closet);
242 }
243
244 pub fn add_tunnel(&mut self, tunnel: Tunnel) {
246 self.tunnels.push(tunnel);
247 }
248
249 pub fn drawers_in_scope(&self, wing: &str, room: &str) -> Vec<&Drawer> {
251 self.wings
252 .get(wing)
253 .and_then(|w| w.rooms.get(room))
254 .map(|r| r.drawers.iter().collect())
255 .unwrap_or_default()
256 }
257
258 pub fn tunnels_from(&self, wing: &str, room: &str) -> Vec<&Tunnel> {
260 self.tunnels
261 .iter()
262 .filter(|t| t.source_wing == wing && t.source_room == room)
263 .collect()
264 }
265
266 pub fn find_drawer(&self, id: &str) -> Option<&Drawer> {
268 for wing in self.wings.values() {
269 for room in wing.rooms.values() {
270 if let Some(d) = room.drawers.iter().find(|d| d.id == id) {
271 return Some(d);
272 }
273 }
274 }
275 None
276 }
277
278 pub fn find_drawer_mut(&mut self, id: &str) -> Option<&mut Drawer> {
280 for wing in self.wings.values_mut() {
281 for room in wing.rooms.values_mut() {
282 if let Some(d) = room.drawers.iter_mut().find(|d| d.id == id) {
283 return Some(d);
284 }
285 }
286 }
287 None
288 }
289
290 pub fn linked_active_ids(&self, drawer_id: &str) -> Vec<String> {
292 self.find_drawer(drawer_id)
293 .map(|d| {
294 d.linked_ids.iter()
295 .filter(|lid| {
296 self.find_drawer(lid)
297 .map(|linked| linked.active)
298 .unwrap_or(false)
299 })
300 .cloned()
301 .collect()
302 })
303 .unwrap_or_default()
304 }
305
306 pub fn stats(&self) -> (usize, usize, usize) {
308 let mut total = 0;
309 let mut active = 0;
310 let mut superseded = 0;
311 for wing in self.wings.values() {
312 for room in wing.rooms.values() {
313 for d in &room.drawers {
314 total += 1;
315 if d.active {
316 active += 1;
317 } else {
318 superseded += 1;
319 }
320 }
321 }
322 }
323 (total, active, superseded)
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn drawer_new_defaults() {
333 let d = Drawer::new("d1".into(), "hello".into(), "dev".into(), "rust".into(), "main.rs".into(), 0);
334 assert_eq!(d.id, "d1");
335 assert_eq!(d.content, "hello");
336 assert_eq!(d.confidence, 0.7);
337 assert!(d.active);
338 assert_eq!(d.access_count, 0);
339 assert!(d.linked_ids.is_empty());
340 assert!(d.superseded_by.is_none());
341 assert!(d.reinforcements.is_empty());
342 }
343
344 #[test]
345 fn drawer_effective_confidence_fresh() {
346 let d = Drawer::new("d1".into(), "c".into(), "w".into(), "r".into(), "f".into(), 0);
347 let eff = d.effective_confidence();
349 assert!((0.65..=0.75).contains(&eff), "expected ~0.7, got {}", eff);
350 }
351
352 #[test]
353 fn drawer_effective_confidence_with_access() {
354 let mut d = Drawer::new("d1".into(), "c".into(), "w".into(), "r".into(), "f".into(), 0);
355 d.access_count = 10;
356 d.accessed_at = Some(chrono::Utc::now());
357 let eff = d.effective_confidence();
358 assert!(eff > 0.7, "access boost should raise confidence, got {}", eff);
359 }
360
361 #[test]
362 fn drawer_touch() {
363 let mut d = Drawer::new("d1".into(), "c".into(), "w".into(), "r".into(), "f".into(), 0);
364 d.touch();
365 assert_eq!(d.access_count, 1);
366 assert!(d.accessed_at.is_some());
367 }
368
369 #[test]
370 fn drawer_reinforce() {
371 let mut d = Drawer::new("d1".into(), "c".into(), "w".into(), "r".into(), "f".into(), 0);
372 d.reinforce(0.5, "positive feedback");
373 assert_eq!(d.reinforcements.len(), 1);
374 assert_eq!(d.reinforcements[0].score, 0.5);
375 d.reinforce(2.0, "clamped");
377 assert_eq!(d.reinforcements[1].score, 1.0);
378 }
379
380 #[test]
381 fn drawer_decay_confidence() {
382 let mut d = Drawer::new("d1".into(), "c".into(), "w".into(), "r".into(), "f".into(), 0);
383 d.decay_confidence(0.3);
384 assert!((d.confidence - 0.4).abs() < 0.01);
385 d.decay_confidence(1.0);
386 assert_eq!(d.confidence, 0.0); }
388
389 #[test]
390 fn drawer_supersede() {
391 let mut d = Drawer::new("d1".into(), "c".into(), "w".into(), "r".into(), "f".into(), 0);
392 d.supersede("d2");
393 assert_eq!(d.superseded_by, Some("d2".to_string()));
394 assert!(!d.active);
395 }
396
397 #[test]
398 fn drawer_link_to_no_duplicates() {
399 let mut d = Drawer::new("d1".into(), "c".into(), "w".into(), "r".into(), "f".into(), 0);
400 d.link_to("d2");
401 d.link_to("d2"); d.link_to("d3");
403 assert_eq!(d.linked_ids.len(), 2);
404 }
405
406 #[test]
407 fn drawer_effective_confidence_with_reinforcements() {
408 let mut d = Drawer::new("d1".into(), "c".into(), "w".into(), "r".into(), "f".into(), 0);
409 d.reinforce(0.5, "good");
410 d.reinforce(0.5, "good");
411 d.reinforce(0.5, "good");
412 let eff = d.effective_confidence();
413 assert!(eff > 0.7, "positive reinforcement should boost, got {}", eff);
414 }
415
416 #[test]
417 fn tunnel_scopes() {
418 let t = Tunnel {
419 source_wing: "dev".into(),
420 source_room: "rust".into(),
421 target_wing: "ops".into(),
422 target_room: "deploy".into(),
423 relation: "implements".into(),
424 };
425 assert_eq!(t.source_scope(), "dev/rust");
426 assert_eq!(t.target_scope(), "ops/deploy");
427 }
428
429 #[test]
430 fn memory_taxonomy_ingest_short_text() {
431 let mut tax = MemoryTaxonomy::new();
432 tax.ingest("dev", "rust", "main.rs", "Hello world");
433 assert_eq!(tax.wings.len(), 1);
434 assert_eq!(tax.drawers_in_scope("dev", "rust").len(), 1);
435 }
436
437 #[test]
438 fn memory_taxonomy_ingest_long_text_chunks() {
439 let mut tax = MemoryTaxonomy::new();
440 let long_text: String = (0..2000).map(|i| (b'a' + (i % 26) as u8) as char).collect();
442 tax.ingest("dev", "rust", "big.rs", &long_text);
443 let drawers = tax.drawers_in_scope("dev", "rust");
444 assert!(drawers.len() >= 2, "expected multiple chunks, got {}", drawers.len());
445 let mut ids: Vec<&str> = drawers.iter().map(|d| d.id.as_str()).collect();
447 ids.sort();
448 ids.dedup();
449 assert_eq!(ids.len(), drawers.len());
450 }
451
452 #[test]
453 fn memory_taxonomy_add_tunnel() {
454 let mut tax = MemoryTaxonomy::new();
455 tax.add_tunnel(Tunnel {
456 source_wing: "a".into(), source_room: "b".into(),
457 target_wing: "c".into(), target_room: "d".into(),
458 relation: "references".into(),
459 });
460 assert_eq!(tax.tunnels.len(), 1);
461 assert_eq!(tax.tunnels_from("a", "b").len(), 1);
462 assert_eq!(tax.tunnels_from("c", "d").len(), 0); }
464
465 #[test]
466 fn memory_taxonomy_find_drawer() {
467 let mut tax = MemoryTaxonomy::new();
468 tax.ingest("dev", "rust", "main.rs", "test content");
469 let drawer = &tax.drawers_in_scope("dev", "rust")[0];
470 let id = drawer.id.clone();
471 assert!(tax.find_drawer(&id).is_some());
472 assert!(tax.find_drawer("nonexistent").is_none());
473 }
474
475 #[test]
476 fn memory_taxonomy_find_drawer_mut_and_supersede() {
477 let mut tax = MemoryTaxonomy::new();
478 tax.ingest("dev", "rust", "main.rs", "content");
479 let id = tax.drawers_in_scope("dev", "rust")[0].id.clone();
480 tax.find_drawer_mut(&id).unwrap().supersede("new_id");
481 assert!(!tax.find_drawer(&id).unwrap().active);
482 let (total, active, superseded) = tax.stats();
483 assert_eq!(total, 1);
484 assert_eq!(active, 0);
485 assert_eq!(superseded, 1);
486 }
487
488 #[test]
489 fn memory_taxonomy_linked_active_ids() {
490 let mut tax = MemoryTaxonomy::new();
491 tax.ingest("dev", "rust", "a.rs", "content A");
492 tax.ingest("dev", "rust", "b.rs", "content B");
493 let id_a = tax.drawers_in_scope("dev", "rust")[0].id.clone();
494 let id_b = tax.drawers_in_scope("dev", "rust")[1].id.clone();
495
496 tax.find_drawer_mut(&id_a).unwrap().link_to(&id_b);
498 let linked = tax.linked_active_ids(&id_a);
499 assert_eq!(linked, vec![id_b.clone()]);
500
501 tax.find_drawer_mut(&id_b).unwrap().supersede("replacement");
503 assert!(tax.linked_active_ids(&id_a).is_empty());
504 }
505
506 #[test]
507 fn memory_taxonomy_add_closet() {
508 let mut tax = MemoryTaxonomy::new();
509 tax.add_closet("dev", "rust", Closet {
510 topic: "ownership".into(),
511 entities: vec!["borrow checker".into()],
512 drawer_ids: vec!["d1".into()],
513 });
514 let wing = tax.wings.get("dev").unwrap();
515 let room = wing.rooms.get("rust").unwrap();
516 assert_eq!(room.closets.len(), 1);
517 assert_eq!(room.closets[0].topic, "ownership");
518 }
519
520 #[test]
521 fn memory_taxonomy_stats_empty() {
522 let tax = MemoryTaxonomy::new();
523 assert_eq!(tax.stats(), (0, 0, 0));
524 }
525
526 #[test]
527 fn memory_taxonomy_serialization_roundtrip() {
528 let mut tax = MemoryTaxonomy::new();
529 tax.ingest("dev", "rust", "main.rs", "hello world");
530 let drawer = &tax.drawers_in_scope("dev", "rust")[0];
532 let json = serde_json::to_string(drawer).unwrap();
533 let deserialized: Drawer = serde_json::from_str(&json).unwrap();
534 assert_eq!(deserialized.content, drawer.content);
535 assert_eq!(deserialized.wing, drawer.wing);
536 }
537}