//! Pure domain entity for long-term agent memory. //! //! A `Memory` entry stores a named, kinded piece of information (lesson, //! reference, fact) with frontmatter metadata and free-form markdown //! content. Memories are persisted as individual `.md` files with YAML //! frontmatter. //! //! # Architecture //! This is a pure data structure with **no I/O logic**. Load/save //! responsibilities live in `MemoryRepository` (domain::repository). //! //! ## Utility Functions //! - `slugify()` — converts a name string into a filesystem-safe slug //! - `path()` — computes the on-disk path for a given memory name //! //! Both are pure computations that take parameters and perform no I/O. use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; /// A single memory entry with frontmatter metadata and markdown content. /// /// ## Fields /// - `name` — unique identifier / title for this memory /// - `description` — short summary of what this memory contains /// - `content` — free-form markdown body /// - `kind` — category/tag (e.g. "lesson", "reference", "fact") /// - `created_at` — Unix timestamp of creation /// - `updated_at` — Unix timestamp of last modification /// - `outcome` — optional outcome of applying this memory /// - `lifecycle` — lifecycle stage (e.g. "active", "archived") /// - `scope` — optional scope qualifier (which session/context this applies to) /// - `before_snippet` — optional context snapshot before memory was applied /// - `after_snippet` — optional context snapshot after memory was applied /// - `provenances` — list of origin identifiers that created or confirmed this memory #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Memory { pub name: String, pub description: String, pub content: String, pub kind: String, pub created_at: i64, pub updated_at: i64, pub outcome: Option, pub lifecycle: String, pub scope: Option, pub before_snippet: Option, pub after_snippet: Option, pub provenances: Vec, } impl Memory { /// Convert an arbitrary string into a filesystem-safe slug. /// /// Flow: lowercase → replace non-alphanumeric chars with `-` → /// collapse/trim repeated `-`. /// /// Returns `None` if the result is empty or exceeds 80 characters. pub fn slugify(s: &str) -> Option { // Phase 1: replace every non-alphanumeric character with '-' let slug: String = s .to_lowercase() .chars() .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }) .collect(); // Phase 2: collapse consecutive '-' separators let slug: String = slug .split('-') .filter(|s| !s.is_empty()) .collect::>() .join("-"); if slug.is_empty() || slug.len() > 80 { return None; } Some(slug) } /// Compute the on-disk path for a memory of the given name. /// /// ## Parameters /// - `memory_dir` — the base directory for memory storage /// - `name` — the memory name (will be slugified internally) /// /// Falls back to `"memory.md"` when the name slugifies to an empty /// or invalid string. /// /// ## Pure Computation /// This function performs **no I/O** — it only computes a path. pub fn path(memory_dir: &Path, name: &str) -> PathBuf { let slug = Self::slugify(name).unwrap_or_else(|| "memory".to_string()); let clean: String = format!("{slug}.md") .chars() .map(|c| { if c.is_ascii_alphanumeric() || c == '.' || c == '-' { c } else { '-' } }) .collect(); let clean = clean.trim_start_matches('.').to_string(); memory_dir.join(if clean.is_empty() { "memory.md".to_string() } else { clean }) } }