umi_memory/memory/
archival.rs

1//! Archival Memory - Tier 3 Long-Term Storage
2//!
3//! TigerStyle: High-level API over StorageBackend.
4//!
5//! # Simulation-First
6//!
7//! Tests are written BEFORE implementation. This file starts with tests
8//! and minimal stubs. Implementation follows to make tests pass.
9//!
10//! # Architecture
11//!
12//! ```text
13//! ┌─────────────────────────────────────────────────────────────┐
14//! │                      ArchivalMemory                          │
15//! │  High-level API for Tier 3 operations                       │
16//! │  - remember(content, type) → stores with auto-naming        │
17//! │  - recall(query, limit) → semantic search                   │
18//! │  - forget(id) → delete                                      │
19//! └─────────────────────────────────────────────────────────────┘
20//!                               │
21//!                               ↓ uses
22//! ┌─────────────────────────────────────────────────────────────┐
23//! │                    StorageBackend Trait                      │
24//! └─────────────────────────────────────────────────────────────┘
25//! ```
26
27use std::sync::Arc;
28
29use crate::storage::{Entity, EntityType, StorageBackend, StorageResult};
30
31// =============================================================================
32// Configuration
33// =============================================================================
34
35/// Configuration for archival memory.
36#[derive(Debug, Clone)]
37pub struct ArchivalMemoryConfig {
38    /// Default limit for recall operations
39    pub default_recall_limit: usize,
40    /// Maximum content length for auto-naming
41    pub auto_name_max_chars: usize,
42}
43
44impl Default for ArchivalMemoryConfig {
45    fn default() -> Self {
46        Self {
47            default_recall_limit: 10,
48            auto_name_max_chars: 50,
49        }
50    }
51}
52
53// =============================================================================
54// Archival Memory
55// =============================================================================
56
57/// Archival Memory - Tier 3 long-term storage.
58///
59/// TigerStyle:
60/// - High-level API over StorageBackend
61/// - Automatic naming from content
62/// - Generic over backend for testing
63#[derive(Debug)]
64pub struct ArchivalMemory<B: StorageBackend> {
65    backend: Arc<B>,
66    config: ArchivalMemoryConfig,
67}
68
69impl<B: StorageBackend> ArchivalMemory<B> {
70    /// Create a new archival memory with the given backend.
71    pub fn new(backend: B) -> Self {
72        Self {
73            backend: Arc::new(backend),
74            config: ArchivalMemoryConfig::default(),
75        }
76    }
77
78    /// Create with custom configuration.
79    pub fn with_config(backend: B, config: ArchivalMemoryConfig) -> Self {
80        Self {
81            backend: Arc::new(backend),
82            config,
83        }
84    }
85
86    /// Remember something - store in long-term memory.
87    ///
88    /// Automatically generates a name from content if not provided.
89    ///
90    /// # Arguments
91    /// * `content` - The content to remember
92    /// * `entity_type` - Type of entity (Note, Person, Project, etc.)
93    /// * `name` - Optional name (auto-generated if None)
94    ///
95    /// # Returns
96    /// The stored entity
97    pub async fn remember(
98        &self,
99        content: &str,
100        entity_type: EntityType,
101        name: Option<&str>,
102    ) -> StorageResult<Entity> {
103        // Generate name from content if not provided
104        let name = match name {
105            Some(n) => n.to_string(),
106            None => self.auto_name(content),
107        };
108
109        // Preconditions
110        assert!(!content.is_empty(), "content must not be empty");
111        assert!(!name.is_empty(), "name must not be empty");
112
113        let entity = Entity::new(entity_type, name, content.to_string());
114
115        self.backend.store_entity(&entity).await?;
116
117        Ok(entity)
118    }
119
120    /// Recall memories matching a query.
121    ///
122    /// # Arguments
123    /// * `query` - Search query
124    /// * `limit` - Maximum results (None uses default)
125    ///
126    /// # Returns
127    /// Matching entities
128    pub async fn recall(&self, query: &str, limit: Option<usize>) -> StorageResult<Vec<Entity>> {
129        let limit = limit.unwrap_or(self.config.default_recall_limit);
130        self.backend.search(query, limit).await
131    }
132
133    /// Forget a memory by ID.
134    ///
135    /// # Returns
136    /// True if memory existed and was forgotten
137    pub async fn forget(&self, id: &str) -> StorageResult<bool> {
138        self.backend.delete_entity(id).await
139    }
140
141    /// Get a specific memory by ID.
142    pub async fn get(&self, id: &str) -> StorageResult<Option<Entity>> {
143        self.backend.get_entity(id).await
144    }
145
146    /// List memories of a given type.
147    pub async fn list(
148        &self,
149        entity_type: Option<EntityType>,
150        limit: usize,
151        offset: usize,
152    ) -> StorageResult<Vec<Entity>> {
153        self.backend.list_entities(entity_type, limit, offset).await
154    }
155
156    /// Count memories of a given type.
157    pub async fn count(&self, entity_type: Option<EntityType>) -> StorageResult<usize> {
158        self.backend.count_entities(entity_type).await
159    }
160
161    /// Update an existing memory.
162    pub async fn update(&self, entity: &Entity) -> StorageResult<String> {
163        self.backend.store_entity(entity).await
164    }
165
166    /// Remember with full entity builder control.
167    pub async fn remember_entity(&self, entity: Entity) -> StorageResult<Entity> {
168        self.backend.store_entity(&entity).await?;
169        Ok(entity)
170    }
171
172    /// Generate a name from content.
173    fn auto_name(&self, content: &str) -> String {
174        // Take first line or first N characters
175        let first_line = content.lines().next().unwrap_or(content);
176        let trimmed = first_line.trim();
177
178        if trimmed.len() <= self.config.auto_name_max_chars {
179            trimmed.to_string()
180        } else {
181            // Truncate at word boundary if possible
182            let truncated = &trimmed[..self.config.auto_name_max_chars];
183            if let Some(last_space) = truncated.rfind(' ') {
184                format!("{}...", &truncated[..last_space])
185            } else {
186                format!("{}...", truncated)
187            }
188        }
189    }
190
191    /// Get the underlying backend (for testing).
192    #[cfg(test)]
193    pub fn backend(&self) -> &B {
194        &self.backend
195    }
196}
197
198// =============================================================================
199// TESTS - Written FIRST (Simulation-First)
200// =============================================================================
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::dst::SimConfig;
206    use crate::storage::SimStorageBackend;
207
208    fn create_memory() -> ArchivalMemory<SimStorageBackend> {
209        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
210        ArchivalMemory::new(backend)
211    }
212
213    // =========================================================================
214    // Remember Tests
215    // =========================================================================
216
217    #[tokio::test]
218    async fn test_remember_with_name() {
219        let memory = create_memory();
220
221        let entity = memory
222            .remember("Alice is my friend", EntityType::Person, Some("Alice"))
223            .await
224            .unwrap();
225
226        assert_eq!(entity.name, "Alice");
227        assert_eq!(entity.content, "Alice is my friend");
228        assert_eq!(entity.entity_type, EntityType::Person);
229    }
230
231    #[tokio::test]
232    async fn test_remember_auto_name() {
233        let memory = create_memory();
234
235        let entity = memory
236            .remember(
237                "This is a note about something important",
238                EntityType::Note,
239                None,
240            )
241            .await
242            .unwrap();
243
244        assert_eq!(entity.name, "This is a note about something important");
245        assert_eq!(entity.entity_type, EntityType::Note);
246    }
247
248    #[tokio::test]
249    async fn test_remember_auto_name_truncates() {
250        let memory = create_memory();
251
252        let long_content = "This is a very long piece of content that exceeds the maximum auto-name length and should be truncated at a word boundary";
253        let entity = memory
254            .remember(long_content, EntityType::Note, None)
255            .await
256            .unwrap();
257
258        assert!(entity.name.len() <= 55); // 50 + "..."
259        assert!(entity.name.ends_with("..."));
260    }
261
262    #[tokio::test]
263    async fn test_remember_auto_name_first_line() {
264        let memory = create_memory();
265
266        let multiline = "First line title\nSecond line with more content\nThird line";
267        let entity = memory
268            .remember(multiline, EntityType::Note, None)
269            .await
270            .unwrap();
271
272        assert_eq!(entity.name, "First line title");
273    }
274
275    // =========================================================================
276    // Recall Tests
277    // =========================================================================
278
279    #[tokio::test]
280    async fn test_recall_finds_matching() {
281        let memory = create_memory();
282
283        memory
284            .remember(
285                "Alice is a software engineer",
286                EntityType::Person,
287                Some("Alice"),
288            )
289            .await
290            .unwrap();
291        memory
292            .remember("Bob is a designer", EntityType::Person, Some("Bob"))
293            .await
294            .unwrap();
295
296        let results = memory.recall("software", None).await.unwrap();
297        assert_eq!(results.len(), 1);
298        assert_eq!(results[0].name, "Alice");
299    }
300
301    #[tokio::test]
302    async fn test_recall_respects_limit() {
303        let memory = create_memory();
304
305        for i in 0..10 {
306            memory
307                .remember(&format!("Note {} about coding", i), EntityType::Note, None)
308                .await
309                .unwrap();
310        }
311
312        let results = memory.recall("coding", Some(3)).await.unwrap();
313        assert_eq!(results.len(), 3);
314    }
315
316    #[tokio::test]
317    async fn test_recall_empty_results() {
318        let memory = create_memory();
319
320        memory
321            .remember("Something about Rust", EntityType::Note, Some("Rust"))
322            .await
323            .unwrap();
324
325        let results = memory.recall("Python", None).await.unwrap();
326        assert!(results.is_empty());
327    }
328
329    // =========================================================================
330    // Forget Tests
331    // =========================================================================
332
333    #[tokio::test]
334    async fn test_forget() {
335        let memory = create_memory();
336
337        let entity = memory
338            .remember("Temporary note", EntityType::Note, Some("Temp"))
339            .await
340            .unwrap();
341
342        let forgotten = memory.forget(&entity.id).await.unwrap();
343        assert!(forgotten);
344
345        let retrieved = memory.get(&entity.id).await.unwrap();
346        assert!(retrieved.is_none());
347    }
348
349    #[tokio::test]
350    async fn test_forget_nonexistent() {
351        let memory = create_memory();
352
353        let forgotten = memory.forget("nonexistent-id").await.unwrap();
354        assert!(!forgotten);
355    }
356
357    // =========================================================================
358    // Get Tests
359    // =========================================================================
360
361    #[tokio::test]
362    async fn test_get() {
363        let memory = create_memory();
364
365        let entity = memory
366            .remember("Test content", EntityType::Note, Some("Test"))
367            .await
368            .unwrap();
369
370        let retrieved = memory.get(&entity.id).await.unwrap();
371        assert!(retrieved.is_some());
372
373        let retrieved = retrieved.unwrap();
374        assert_eq!(retrieved.id, entity.id);
375        assert_eq!(retrieved.name, "Test");
376    }
377
378    #[tokio::test]
379    async fn test_get_nonexistent() {
380        let memory = create_memory();
381
382        let retrieved = memory.get("nonexistent").await.unwrap();
383        assert!(retrieved.is_none());
384    }
385
386    // =========================================================================
387    // List and Count Tests
388    // =========================================================================
389
390    #[tokio::test]
391    async fn test_list_all() {
392        let memory = create_memory();
393
394        memory
395            .remember("Person 1", EntityType::Person, Some("Alice"))
396            .await
397            .unwrap();
398        memory
399            .remember("Project 1", EntityType::Project, Some("Umi"))
400            .await
401            .unwrap();
402        memory
403            .remember("Note 1", EntityType::Note, Some("Note"))
404            .await
405            .unwrap();
406
407        let all = memory.list(None, 100, 0).await.unwrap();
408        assert_eq!(all.len(), 3);
409    }
410
411    #[tokio::test]
412    async fn test_list_by_type() {
413        let memory = create_memory();
414
415        memory
416            .remember("Alice", EntityType::Person, Some("Alice"))
417            .await
418            .unwrap();
419        memory
420            .remember("Bob", EntityType::Person, Some("Bob"))
421            .await
422            .unwrap();
423        memory
424            .remember("Umi", EntityType::Project, Some("Umi"))
425            .await
426            .unwrap();
427
428        let people = memory.list(Some(EntityType::Person), 100, 0).await.unwrap();
429        assert_eq!(people.len(), 2);
430
431        let projects = memory
432            .list(Some(EntityType::Project), 100, 0)
433            .await
434            .unwrap();
435        assert_eq!(projects.len(), 1);
436    }
437
438    #[tokio::test]
439    async fn test_count() {
440        let memory = create_memory();
441
442        memory
443            .remember("A", EntityType::Note, Some("A"))
444            .await
445            .unwrap();
446        memory
447            .remember("B", EntityType::Note, Some("B"))
448            .await
449            .unwrap();
450        memory
451            .remember("C", EntityType::Person, Some("C"))
452            .await
453            .unwrap();
454
455        assert_eq!(memory.count(None).await.unwrap(), 3);
456        assert_eq!(memory.count(Some(EntityType::Note)).await.unwrap(), 2);
457        assert_eq!(memory.count(Some(EntityType::Person)).await.unwrap(), 1);
458    }
459
460    // =========================================================================
461    // Update Tests
462    // =========================================================================
463
464    #[tokio::test]
465    async fn test_update() {
466        let memory = create_memory();
467
468        let mut entity = memory
469            .remember("Original content", EntityType::Note, Some("Note"))
470            .await
471            .unwrap();
472
473        entity.update_content("Updated content".to_string());
474        memory.update(&entity).await.unwrap();
475
476        let retrieved = memory.get(&entity.id).await.unwrap().unwrap();
477        assert_eq!(retrieved.content, "Updated content");
478    }
479}
480
481// =============================================================================
482// DST Tests
483// =============================================================================
484
485#[cfg(test)]
486mod dst_tests {
487    use super::*;
488    use crate::dst::{FaultConfig, FaultType, SimConfig};
489    use crate::storage::SimStorageBackend;
490
491    #[tokio::test]
492    async fn test_remember_with_fault_injection() {
493        let backend = SimStorageBackend::new(SimConfig::with_seed(42))
494            .with_faults(FaultConfig::new(FaultType::StorageWriteFail, 1.0).with_filter("store"));
495        let memory = ArchivalMemory::new(backend);
496
497        let result = memory
498            .remember("Test", EntityType::Note, Some("Test"))
499            .await;
500
501        assert!(result.is_err());
502    }
503
504    #[tokio::test]
505    async fn test_recall_with_fault_injection() {
506        let backend = SimStorageBackend::new(SimConfig::with_seed(42))
507            .with_faults(FaultConfig::new(FaultType::StorageReadFail, 1.0).with_filter("search"));
508        let memory = ArchivalMemory::new(backend);
509
510        let result = memory.recall("test", None).await;
511        assert!(result.is_err());
512    }
513}
514
515// =============================================================================
516// Property-Based Tests
517// =============================================================================
518
519#[cfg(test)]
520mod property_tests {
521    use super::*;
522    use crate::dst::{
523        DeterministicRng, PropertyTest, PropertyTestable, SimClock, SimConfig, TimeAdvanceConfig,
524    };
525    use crate::storage::SimStorageBackend;
526
527    /// Operations on ArchivalMemory
528    #[derive(Debug, Clone)]
529    enum MemoryOp {
530        Remember {
531            content: String,
532            entity_type: EntityType,
533        },
534        Recall {
535            query: String,
536        },
537        Forget {
538            id: String,
539        },
540        Get {
541            id: String,
542        },
543        Count,
544    }
545
546    struct MemoryWrapper {
547        memory: ArchivalMemory<SimStorageBackend>,
548        known_ids: Vec<String>,
549    }
550
551    impl PropertyTestable for MemoryWrapper {
552        type Operation = MemoryOp;
553
554        fn generate_operation(&self, rng: &mut DeterministicRng) -> Self::Operation {
555            let op_type = rng.next_usize(0, 4);
556
557            match op_type {
558                0 => {
559                    let types = EntityType::all();
560                    let type_idx = rng.next_usize(0, types.len() - 1);
561                    MemoryOp::Remember {
562                        content: format!("Content {}", rng.next_usize(0, 999)),
563                        entity_type: types[type_idx],
564                    }
565                }
566                1 => MemoryOp::Recall {
567                    query: format!("Content {}", rng.next_usize(0, 9)),
568                },
569                2 => {
570                    let id = if !self.known_ids.is_empty() && rng.next_bool(0.7) {
571                        let idx = rng.next_usize(0, self.known_ids.len() - 1);
572                        self.known_ids[idx].clone()
573                    } else {
574                        format!("unknown_{}", rng.next_usize(0, 99))
575                    };
576                    MemoryOp::Forget { id }
577                }
578                3 => {
579                    let id = if !self.known_ids.is_empty() && rng.next_bool(0.7) {
580                        let idx = rng.next_usize(0, self.known_ids.len() - 1);
581                        self.known_ids[idx].clone()
582                    } else {
583                        format!("unknown_{}", rng.next_usize(0, 99))
584                    };
585                    MemoryOp::Get { id }
586                }
587                _ => MemoryOp::Count,
588            }
589        }
590
591        fn apply_operation(&mut self, op: &Self::Operation, _clock: &SimClock) {
592            let rt = tokio::runtime::Builder::new_current_thread()
593                .enable_all()
594                .build()
595                .unwrap();
596
597            rt.block_on(async {
598                match op {
599                    MemoryOp::Remember {
600                        content,
601                        entity_type,
602                    } => {
603                        if let Ok(entity) = self.memory.remember(content, *entity_type, None).await
604                        {
605                            self.known_ids.push(entity.id);
606                        }
607                    }
608                    MemoryOp::Recall { query } => {
609                        let _ = self.memory.recall(query, None).await;
610                    }
611                    MemoryOp::Forget { id } => {
612                        if self.memory.forget(id).await.unwrap_or(false) {
613                            self.known_ids.retain(|i| i != id);
614                        }
615                    }
616                    MemoryOp::Get { id } => {
617                        let _ = self.memory.get(id).await;
618                    }
619                    MemoryOp::Count => {
620                        let _ = self.memory.count(None).await;
621                    }
622                }
623            });
624        }
625
626        fn check_invariants(&self) -> Result<(), String> {
627            let rt = tokio::runtime::Builder::new_current_thread()
628                .enable_all()
629                .build()
630                .unwrap();
631
632            rt.block_on(async {
633                let count = self.memory.count(None).await.map_err(|e| e.to_string())?;
634                if count != self.known_ids.len() {
635                    return Err(format!(
636                        "count {} != known_ids.len() {}",
637                        count,
638                        self.known_ids.len()
639                    ));
640                }
641                Ok(())
642            })
643        }
644
645        fn describe_state(&self) -> String {
646            format!("ArchivalMemory {{ known_ids: {} }}", self.known_ids.len())
647        }
648    }
649
650    #[test]
651    fn test_property_invariants() {
652        let backend = SimStorageBackend::new(SimConfig::with_seed(42));
653        let memory = ArchivalMemory::new(backend);
654
655        let wrapper = MemoryWrapper {
656            memory,
657            known_ids: Vec::new(),
658        };
659
660        PropertyTest::new(42)
661            .with_max_operations(200)
662            .with_time_advance(TimeAdvanceConfig::none())
663            .run_and_assert(wrapper);
664    }
665
666    #[test]
667    fn test_property_multi_seed() {
668        for seed in [0, 1, 42, 12345] {
669            let backend = SimStorageBackend::new(SimConfig::with_seed(seed));
670            let memory = ArchivalMemory::new(backend);
671
672            let wrapper = MemoryWrapper {
673                memory,
674                known_ids: Vec::new(),
675            };
676
677            PropertyTest::new(seed)
678                .with_max_operations(100)
679                .with_time_advance(TimeAdvanceConfig::none())
680                .run_and_assert(wrapper);
681        }
682    }
683}