//! Memory use cases (ARCHITECTURE §14.5.1). //! //! - **CRUD**: [`CreateMemory`], [`UpdateMemory`], [`GetMemory`], //! [`ListMemories`], [`DeleteMemory`]. //! - **Navigation** (read-only): [`ReadMemoryIndex`] (structured `MEMORY.md` //! rows) and [`ResolveMemoryLinks`] (a note's outgoing `[[slug]]` links). //! //! Mutating use cases announce a [`DomainEvent`]; the read use cases emit none. use std::sync::Arc; use domain::ports::{EventBus, MemoryQuery, MemoryRecall, MemoryStore}; use domain::{ DomainEvent, MarkdownDoc, Memory, MemoryFrontmatter, MemoryIndexEntry, MemoryLink, MemorySlug, MemoryType, ProjectPath, }; use crate::error::AppError; // --------------------------------------------------------------------------- // CreateMemory // --------------------------------------------------------------------------- /// Input for [`CreateMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateMemoryInput { /// Active project root (the note is stored under its `.ideai/memory/`). pub project_root: ProjectPath, /// Raw slug for the new note (validated into a [`MemorySlug`]). pub name: String, /// Human-readable one-line description (the index hook). Non-empty. pub description: String, /// The note's kind. pub r#type: MemoryType, /// Markdown body of the note. pub content: String, } /// Output of [`CreateMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CreateMemoryOutput { /// The created note. pub memory: Memory, } /// Creates a memory note in the project's store and upserts the index. /// /// Announces [`DomainEvent::MemorySaved`] on success. pub struct CreateMemory { memories: Arc, events: Arc, } impl CreateMemory { /// Builds the use case from its ports. #[must_use] pub fn new(memories: Arc, events: Arc) -> Self { Self { memories, events } } /// Executes creation. /// /// # Errors /// - [`AppError::Invalid`] if `name` is not a valid kebab-case slug or the /// note's invariants are violated (empty description/body), /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: CreateMemoryInput) -> Result { let slug = MemorySlug::new(input.name).map_err(|e| AppError::Invalid(e.to_string()))?; let frontmatter = MemoryFrontmatter { name: slug.clone(), description: input.description, r#type: input.r#type, }; let memory = Memory::new(frontmatter, MarkdownDoc::new(input.content)) .map_err(|e| AppError::Invalid(e.to_string()))?; self.memories.save(&input.project_root, &memory).await?; self.events.publish(DomainEvent::MemorySaved { slug }); Ok(CreateMemoryOutput { memory }) } } // --------------------------------------------------------------------------- // UpdateMemory // --------------------------------------------------------------------------- /// Input for [`UpdateMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateMemoryInput { /// Active project root. pub project_root: ProjectPath, /// Slug of the note to replace. pub slug: MemorySlug, /// New description (the index hook). Non-empty. pub description: String, /// New kind. pub r#type: MemoryType, /// New Markdown body. pub content: String, } /// Output of [`UpdateMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UpdateMemoryOutput { /// The updated note. pub memory: Memory, } /// Replaces a memory note's content (re-validating its invariants). /// /// Announces [`DomainEvent::MemorySaved`] on success. The input carries the full /// note, so no prior `get` is needed — this is replace semantics. pub struct UpdateMemory { memories: Arc, events: Arc, } impl UpdateMemory { /// Builds the use case from its ports. #[must_use] pub fn new(memories: Arc, events: Arc) -> Self { Self { memories, events } } /// Executes the update. /// /// # Errors /// - [`AppError::Invalid`] if the note's invariants are violated (empty /// description/body), /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: UpdateMemoryInput) -> Result { let frontmatter = MemoryFrontmatter { name: input.slug.clone(), description: input.description, r#type: input.r#type, }; let memory = Memory::new(frontmatter, MarkdownDoc::new(input.content)) .map_err(|e| AppError::Invalid(e.to_string()))?; self.memories.save(&input.project_root, &memory).await?; self.events .publish(DomainEvent::MemorySaved { slug: input.slug }); Ok(UpdateMemoryOutput { memory }) } } // --------------------------------------------------------------------------- // ListMemories // --------------------------------------------------------------------------- /// Input for [`ListMemories::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListMemoriesInput { /// Active project root. pub project_root: ProjectPath, } /// Output of [`ListMemories::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ListMemoriesOutput { /// All notes in the project. pub memories: Vec, } /// Lists the memory notes of a project. pub struct ListMemories { memories: Arc, } impl ListMemories { /// Builds the use case. #[must_use] pub fn new(memories: Arc) -> Self { Self { memories } } /// Lists the project's notes. /// /// # Errors /// - [`AppError::Invalid`] if a note's frontmatter is malformed, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: ListMemoriesInput) -> Result { Ok(ListMemoriesOutput { memories: self.memories.list(&input.project_root).await?, }) } } // --------------------------------------------------------------------------- // GetMemory // --------------------------------------------------------------------------- /// Input for [`GetMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct GetMemoryInput { /// Active project root. pub project_root: ProjectPath, /// Slug of the note to read. pub slug: MemorySlug, } /// Output of [`GetMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct GetMemoryOutput { /// The requested note. pub memory: Memory, } /// Reads one memory note by slug. pub struct GetMemory { memories: Arc, } impl GetMemory { /// Builds the use case. #[must_use] pub fn new(memories: Arc) -> Self { Self { memories } } /// Reads the note. /// /// # Errors /// - [`AppError::NotFound`] if no note carries that slug, /// - [`AppError::Invalid`] if the note's frontmatter is malformed, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: GetMemoryInput) -> Result { Ok(GetMemoryOutput { memory: self.memories.get(&input.project_root, &input.slug).await?, }) } } // --------------------------------------------------------------------------- // DeleteMemory // --------------------------------------------------------------------------- /// Input for [`DeleteMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DeleteMemoryInput { /// Active project root. pub project_root: ProjectPath, /// Slug of the note to delete. pub slug: MemorySlug, } /// Deletes a memory note (and removes its index row). /// /// Announces [`DomainEvent::MemoryDeleted`] on success. pub struct DeleteMemory { memories: Arc, events: Arc, } impl DeleteMemory { /// Builds the use case from its ports. #[must_use] pub fn new(memories: Arc, events: Arc) -> Self { Self { memories, events } } /// Deletes the note. /// /// # Errors /// - [`AppError::NotFound`] if no note carries that slug, /// - [`AppError::Store`] on persistence failure. pub async fn execute(&self, input: DeleteMemoryInput) -> Result<(), AppError> { self.memories .delete(&input.project_root, &input.slug) .await?; self.events .publish(DomainEvent::MemoryDeleted { slug: input.slug }); Ok(()) } } // --------------------------------------------------------------------------- // ReadMemoryIndex // --------------------------------------------------------------------------- /// Input for [`ReadMemoryIndex::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReadMemoryIndexInput { /// Active project root. pub project_root: ProjectPath, } /// Output of [`ReadMemoryIndex::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReadMemoryIndexOutput { /// One row per note in the aggregated `MEMORY.md` index. pub entries: Vec, } /// Reads the structured `MEMORY.md` index (drives the graphical memory view). pub struct ReadMemoryIndex { memories: Arc, } impl ReadMemoryIndex { /// Builds the use case. #[must_use] pub fn new(memories: Arc) -> Self { Self { memories } } /// Reads the index rows. /// /// # Errors /// - [`AppError::Store`] on an I/O failure. pub async fn execute( &self, input: ReadMemoryIndexInput, ) -> Result { Ok(ReadMemoryIndexOutput { entries: self.memories.read_index(&input.project_root).await?, }) } } // --------------------------------------------------------------------------- // RecallMemory // --------------------------------------------------------------------------- /// Input for [`RecallMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RecallMemoryInput { /// Active project root. pub project_root: ProjectPath, /// The recall query (often the agent's current working context). pub text: String, /// Approximate token budget bounding the returned entries (`0` ⇒ empty). pub token_budget: usize, } /// Output of [`RecallMemory::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RecallMemoryOutput { /// The recalled index entries, in relevance order, fitting the budget. pub entries: Vec, } /// Recalls the most relevant memory entries for a query within a token budget /// (LOT B, étage 1). Read-only — emits no event. pub struct RecallMemory { recall: Arc, } impl RecallMemory { /// Builds the use case from the [`MemoryRecall`] port. #[must_use] pub fn new(recall: Arc) -> Self { Self { recall } } /// Executes recall. Best-effort: an empty or absent memory yields an empty /// list, and a budget of `0` yields an empty list. /// /// # Errors /// - [`AppError::Store`] on an unexpected I/O failure of the underlying store. pub async fn execute(&self, input: RecallMemoryInput) -> Result { let query = MemoryQuery { text: input.text, token_budget: input.token_budget, }; Ok(RecallMemoryOutput { entries: self.recall.recall(&input.project_root, &query).await?, }) } } // --------------------------------------------------------------------------- // ResolveMemoryLinks // --------------------------------------------------------------------------- /// Input for [`ResolveMemoryLinks::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolveMemoryLinksInput { /// Active project root. pub project_root: ProjectPath, /// Slug of the source note whose outgoing links are resolved. pub slug: MemorySlug, } /// Output of [`ResolveMemoryLinks::execute`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolveMemoryLinksOutput { /// The note's resolved outgoing `[[slug]]` links (broken links dropped). pub links: Vec, } /// Resolves a note's outgoing `[[slug]]` links (drives link navigation). pub struct ResolveMemoryLinks { memories: Arc, } impl ResolveMemoryLinks { /// Builds the use case. #[must_use] pub fn new(memories: Arc) -> Self { Self { memories } } /// Resolves the outgoing links. /// /// # Errors /// - [`AppError::NotFound`] if the source note does not exist, /// - [`AppError::Store`] on persistence failure. pub async fn execute( &self, input: ResolveMemoryLinksInput, ) -> Result { Ok(ResolveMemoryLinksOutput { links: self .memories .resolve_links(&input.project_root, &input.slug) .await?, }) } }