refactor: migrate monolithic crate to Cargo Workspace with Clean Architecture
Transform the single binary crate into a 9-crate workspace monorepo: - Root Cargo.toml as [workspace] manager with resolver = "2" - zesdex-entities: Domain entity types (session, settings, store, message, etc.) - zesdex-utils: Pure utility functions (error, logger, pagination, slug, clipboard) - zesdex-dto: Data Transfer Objects for LLM provider API communication - zesdex-ipc: Unix-socket IPC layer (client/server/framing/protocol) - zesdex-iam: Identity & Access Management (Clean Architecture: domain/application/infrastructure) - zesdex-cms: Content Management (Clean Architecture: domain/application/infrastructure) - zesdex-middleware: HTTP middleware (Auth, CORS, Rate Limiting) - zesdex-libs: Composition root (AppContext, DB init, JWT, Argon2) - zesdex-backend: Main binary entry point + seed/migrate binaries - DevOps: Dockerfile, docker-compose, Nix (flake/shell/default), CI/CD updates - Remove dead root src/ and src-misc/ directories All crate re-exports maintain backward compatibility with original crate::model::*, crate::dto::*, crate::ipc::* module paths. Feature crates enforce strict layer separation: domain -> application -> infrastructure with generic trait-based dependency injection.
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
//! Markdown file–backed `MemoryRepository`.
|
||||
//!
|
||||
//! Each memory is stored as a `.md` file with YAML-ish frontmatter.
|
||||
//! Filenames are derived from the memory's `name` via slugification.
|
||||
//!
|
||||
//! Frontmatter fields parsed from `---\n...\n---\n` header:
|
||||
//! name, description, kind, created_at, updated_at, lifecycle,
|
||||
//! outcome, scope, before, after, provenances
|
||||
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::domain::memory::Memory;
|
||||
use crate::domain::repository::MemoryRepository;
|
||||
|
||||
/// Persists `Memory` as markdown files with YAML-ish frontmatter.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MarkdownMemoryRepository;
|
||||
|
||||
impl MarkdownMemoryRepository {
|
||||
/// Create a new repository instance.
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Build the frontmatter lines for a memory.
|
||||
fn build_frontmatter(memory: &Memory) -> String {
|
||||
let outcome_line = memory
|
||||
.outcome
|
||||
.as_ref()
|
||||
.map(|o| format!("outcome: {o}\n"))
|
||||
.unwrap_or_default();
|
||||
let scope_line = memory
|
||||
.scope
|
||||
.as_ref()
|
||||
.map(|s| format!("scope: {s}\n"))
|
||||
.unwrap_or_default();
|
||||
let before_line = memory
|
||||
.before_snippet
|
||||
.as_ref()
|
||||
.map(|s| format!("before: {s}\n"))
|
||||
.unwrap_or_default();
|
||||
let after_line = memory
|
||||
.after_snippet
|
||||
.as_ref()
|
||||
.map(|s| format!("after: {s}\n"))
|
||||
.unwrap_or_default();
|
||||
let prov_line = if memory.provenances.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("provenances: {}\n", memory.provenances.join(", "))
|
||||
};
|
||||
format!(
|
||||
"name: {name}\ndescription: {desc}\nkind: {kind}\n\
|
||||
created_at: {created}\nupdated_at: {updated}\nlifecycle: {lifecycle}\n\
|
||||
{outcome}{scope}{before}{after}{prov}",
|
||||
name = memory.name,
|
||||
desc = memory.description,
|
||||
kind = memory.kind,
|
||||
created = memory.created_at,
|
||||
updated = memory.updated_at,
|
||||
lifecycle = memory.lifecycle,
|
||||
outcome = outcome_line,
|
||||
scope = scope_line,
|
||||
before = before_line,
|
||||
after = after_line,
|
||||
prov = prov_line,
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse frontmatter lines into a `HashMap`.
|
||||
fn parse_frontmatter(front: &str) -> HashMap<String, String> {
|
||||
front
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let mut it = l.splitn(2, ':');
|
||||
Some((
|
||||
it.next()?.trim().to_string(),
|
||||
it.next()?.trim().to_string(),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parse a memory file's contents (frontmatter + body) into a `Memory`.
|
||||
fn parse(content: &str) -> std::io::Result<Memory> {
|
||||
let content = content.strip_prefix("---\n").unwrap_or(content);
|
||||
let parts: Vec<&str> = content.splitn(2, "\n---\n").collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"missing frontmatter",
|
||||
));
|
||||
}
|
||||
let front = Self::parse_frontmatter(parts[0]);
|
||||
let body = parts.get(1).unwrap_or(&"").trim().to_string();
|
||||
Ok(Memory {
|
||||
name: front.get("name").cloned().unwrap_or_default(),
|
||||
description: front.get("description").cloned().unwrap_or_default(),
|
||||
content: body,
|
||||
kind: front
|
||||
.get("kind")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "reference".to_string()),
|
||||
created_at: front
|
||||
.get("created_at")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
updated_at: front
|
||||
.get("updated_at")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
outcome: front.get("outcome").cloned().filter(|s| !s.is_empty()),
|
||||
lifecycle: front
|
||||
.get("lifecycle")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "new".to_string()),
|
||||
scope: front.get("scope").cloned().filter(|s| !s.is_empty()),
|
||||
before_snippet: front.get("before").cloned().filter(|s| !s.is_empty()),
|
||||
after_snippet: front.get("after").cloned().filter(|s| !s.is_empty()),
|
||||
provenances: front
|
||||
.get("provenances")
|
||||
.cloned()
|
||||
.map(|s| {
|
||||
s.split(", ")
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryRepository for MarkdownMemoryRepository {
|
||||
fn list(&self, memory_dir: &Path) -> Result<Vec<String>> {
|
||||
let Ok(entries) = std::fs::read_dir(memory_dir) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let slugs: Vec<String> = entries
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter(|e| e.path().extension().is_some_and(|x| x == "md"))
|
||||
.filter_map(|e| {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
// Skip special summary file
|
||||
if name == "MEMORY.md" {
|
||||
return None;
|
||||
}
|
||||
name.strip_suffix(".md").map(std::string::ToString::to_string)
|
||||
})
|
||||
.collect();
|
||||
Ok(slugs)
|
||||
}
|
||||
|
||||
fn load(&self, memory_dir: &Path, name: &str) -> Result<Memory> {
|
||||
let path = Memory::path(memory_dir, name);
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("failed to read memory '{name}' at '{}'", path.display()))?;
|
||||
let memory = Self::parse(&content)
|
||||
.map_err(|e| anyhow::anyhow!("failed to parse memory '{name}': {e}"))?;
|
||||
Ok(memory)
|
||||
}
|
||||
|
||||
fn save(&self, memory_dir: &Path, memory: &Memory) -> Result<()> {
|
||||
let path = Memory::path(memory_dir, &memory.name);
|
||||
let parent = path.parent().unwrap();
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create memory dir '{}'", parent.display()))?;
|
||||
|
||||
let frontmatter = Self::build_frontmatter(memory);
|
||||
let content = format!("---\n{frontmatter}---\n\n{}", memory.content);
|
||||
|
||||
let tmp = parent.join(format!(".{}.tmp", uuid::Uuid::new_v4()));
|
||||
{
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&tmp)
|
||||
.with_context(|| format!("failed to write temp file '{}'", tmp.display()))?;
|
||||
f.write_all(content.as_bytes())?;
|
||||
f.sync_all()?;
|
||||
}
|
||||
std::fs::rename(&tmp, &path)
|
||||
.with_context(|| format!("failed to rename '{}' -> '{}'", tmp.display(), path.display()))?;
|
||||
if let Some(p) = path.parent() {
|
||||
if let Ok(d) = std::fs::File::open(p) {
|
||||
let _ = d.sync_all();
|
||||
}
|
||||
}
|
||||
tracing::debug!("memory saved to '{}'", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete(&self, memory_dir: &Path, name: &str) -> Result<()> {
|
||||
let path = Memory::path(memory_dir, name);
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path)
|
||||
.with_context(|| format!("failed to delete memory '{name}' at '{}'", path.display()))?;
|
||||
tracing::debug!("memory deleted: '{}'", path.display());
|
||||
} else {
|
||||
tracing::warn!("memory '{name}' not found at '{}', skipping delete", path.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user