use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PalaceId(pub String);
impl PalaceId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for PalaceId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Palace {
pub id: PalaceId,
pub name: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub data_dir: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Wing {
pub id: Uuid,
pub palace_id: PalaceId,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum RoomType {
Frontend,
Backend,
Testing,
Planning,
Documentation,
Research,
Configuration,
Meetings,
General,
Custom(String),
}
impl RoomType {
pub fn parse(name: &str) -> Self {
match name.to_lowercase().as_str() {
"frontend" => RoomType::Frontend,
"backend" => RoomType::Backend,
"testing" | "tests" | "test" => RoomType::Testing,
"planning" => RoomType::Planning,
"documentation" | "docs" | "doc" => RoomType::Documentation,
"research" => RoomType::Research,
"configuration" | "config" => RoomType::Configuration,
"meetings" | "meeting" => RoomType::Meetings,
"general" | "" => RoomType::General,
other => RoomType::Custom(other.to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Room {
pub id: Uuid,
pub wing_id: Uuid,
pub room_type: RoomType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum DrawerType {
UserFact,
SessionEvent,
AgentNote,
Commit,
#[default]
Unknown,
}
impl DrawerType {
pub fn as_str(&self) -> &'static str {
match self {
DrawerType::UserFact => "UserFact",
DrawerType::SessionEvent => "SessionEvent",
DrawerType::AgentNote => "AgentNote",
DrawerType::Commit => "Commit",
DrawerType::Unknown => "Unknown",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Drawer {
pub id: Uuid,
pub room_id: Uuid,
pub content: String,
pub importance: f32,
pub source_file: Option<PathBuf>,
pub created_at: DateTime<Utc>,
pub tags: Vec<String>,
#[serde(default)]
pub last_accessed_at: Option<DateTime<Utc>>,
#[serde(default)]
pub access_count: u32,
#[serde(default)]
pub drawer_type: DrawerType,
#[serde(default)]
pub expires_at: Option<DateTime<Utc>>,
}
impl Drawer {
pub fn new(room_id: Uuid, content: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
room_id,
content: content.into(),
importance: 0.5,
source_file: None,
created_at: Utc::now(),
tags: Vec::new(),
last_accessed_at: None,
access_count: 0,
drawer_type: DrawerType::Unknown,
expires_at: None,
}
}
pub fn with_type(mut self, drawer_type: DrawerType) -> Self {
self.drawer_type = drawer_type;
if drawer_type == DrawerType::SessionEvent && self.expires_at.is_none() {
self.expires_at = Some(self.created_at + chrono::Duration::days(7));
}
self
}
pub fn accumulated_boost(&self, config: &crate::memory_core::decay::DecayConfig) -> f32 {
(self.access_count as f32 * config.access_boost).min(config.access_boost_cap)
}
pub fn record_access(&mut self) {
self.last_accessed_at = Some(Utc::now());
self.access_count = self.access_count.saturating_add(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drawer_new_has_default_importance() {
let d = Drawer::new(Uuid::new_v4(), "hello");
assert_eq!(d.importance, 0.5);
assert_eq!(d.content, "hello");
assert!(d.tags.is_empty());
}
#[test]
fn room_type_parse() {
assert_eq!(RoomType::parse("backend"), RoomType::Backend);
assert_eq!(RoomType::parse("Backend"), RoomType::Backend);
assert_eq!(RoomType::parse("docs"), RoomType::Documentation);
assert_eq!(RoomType::parse("general"), RoomType::General);
assert_eq!(RoomType::parse("ops"), RoomType::Custom("ops".to_string()));
}
#[test]
fn drawer_type_as_str_matches_variant() {
assert_eq!(DrawerType::UserFact.as_str(), "UserFact");
assert_eq!(DrawerType::SessionEvent.as_str(), "SessionEvent");
assert_eq!(DrawerType::AgentNote.as_str(), "AgentNote");
assert_eq!(DrawerType::Commit.as_str(), "Commit");
assert_eq!(DrawerType::Unknown.as_str(), "Unknown");
}
#[test]
fn drawer_with_type_sets_session_ttl() {
let d =
Drawer::new(Uuid::new_v4(), "auto-captured event").with_type(DrawerType::SessionEvent);
assert_eq!(d.drawer_type, DrawerType::SessionEvent);
let ttl = d.expires_at.expect("session events get a TTL");
let delta = ttl - d.created_at;
assert!(delta.num_seconds() >= 6 * 24 * 3600);
let fact = Drawer::new(Uuid::new_v4(), "x").with_type(DrawerType::UserFact);
assert!(fact.expires_at.is_none(), "user facts must not expire");
}
#[test]
fn drawer_type_serde_default_is_unknown() {
let json = serde_json::json!({
"id": Uuid::new_v4(),
"room_id": Uuid::new_v4(),
"content": "legacy",
"importance": 0.5,
"source_file": null,
"created_at": Utc::now().to_rfc3339(),
"tags": [],
});
let d: Drawer = serde_json::from_value(json).expect("legacy decode");
assert_eq!(d.drawer_type, DrawerType::Unknown);
assert!(d.expires_at.is_none());
}
#[test]
fn palace_id_display_matches_str() {
let id = PalaceId::new("trusty-memory");
assert_eq!(id.to_string(), "trusty-memory");
assert_eq!(id.as_str(), "trusty-memory");
}
}