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:
asepharyana
2026-07-17 09:08:41 +07:00
parent 86cc412395
commit be0a9582bb
248 changed files with 7901 additions and 1505 deletions
@@ -0,0 +1,173 @@
//! HTTP handler functions for CMS endpoints.
//!
//! Each handler takes a service trait (via generics or trait objects) and
//! returns domain-level results. These functions are agnostic about the
//! HTTP framework — callers (e.g. Axum routes) are responsible for mapping
//! `Result` into HTTP responses.
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
use anyhow::{Context, Result};
use crate::domain::memory::Memory;
use crate::domain::service::{MemoryService, SettingsService};
use crate::domain::settings::Settings;
use super::dto::{
MemoryCreateRequest, MemoryResponse, SettingsResponse, SettingsUpdateRequest,
};
/// Handle `GET /settings`
///
/// Returns the current settings as a `SettingsResponse`.
pub fn handle_get_settings<S: SettingsService>(service: &S) -> Result<SettingsResponse> {
let settings = service
.load_settings()
.context("failed to load settings")?;
Ok(SettingsResponse::from(settings))
}
/// Handle `PUT /settings`
///
/// Applies the partial update from `req` to the current settings, persists
/// the result, and returns the updated `SettingsResponse`.
pub fn handle_update_settings<S: SettingsService>(
service: &S,
req: SettingsUpdateRequest,
) -> Result<SettingsResponse> {
let mut settings: Settings = service
.load_settings()
.context("failed to load current settings for update")?;
// Apply partial updates
if let Some(val) = req.internet_mode {
settings.internet_mode = match val.as_str() {
"Off" => crate::domain::settings::InternetMode::Off,
"ReadOnly" => crate::domain::settings::InternetMode::ReadOnly,
"Full" => crate::domain::settings::InternetMode::Full,
_ => {
return Err(anyhow::anyhow!(
"invalid internet_mode '{}'; expected Off, ReadOnly, or Full",
val
));
}
};
}
if let Some(val) = req.provider {
settings.provider = val;
}
if let Some(val) = req.model {
settings.model = val;
}
if let Some(val) = req.api_keys {
settings.api_keys = val;
}
if let Some(val) = req.max_tokens {
settings.max_tokens = val;
}
if let Some(val) = req.temperature {
settings.temperature = val;
}
if let Some(val) = req.review_max_lessons_per_run {
settings.review_max_lessons_per_run = val;
}
if let Some(val) = req.adaptive_review_max_skip {
settings.adaptive_review_max_skip = val;
}
if let Some(val) = req.verify_command {
settings.verify_command = val;
}
if let Some(val) = req.verify_timeout_ms {
settings.verify_timeout_ms = val;
}
if let Some(val) = req.workflow_max_concurrency {
settings.workflow_max_concurrency = val;
}
if let Some(val) = req.review_enabled {
settings.flags.review_enabled = val;
}
if let Some(val) = req.session_archive_enabled {
settings.flags.session_archive_enabled = val;
}
if let Some(val) = req.lsp_auto_provision {
settings.flags.lsp_auto_provision = val;
}
if let Some(val) = req.lsp_languages {
settings.lsp_languages = val;
}
if let Some(val) = req.hive_mind_node_timeout_ms {
settings.hive_mind_node_timeout_ms = val;
}
service
.save_settings(&settings)
.context("failed to save updated settings")?;
Ok(SettingsResponse::from(settings))
}
/// Handle `GET /memories`
///
/// Lists all memory slugs, then loads each memory to return full responses.
pub fn handle_list_memories<M: MemoryService>(service: &M) -> Result<Vec<MemoryResponse>> {
let slugs = service
.list_memories()
.context("failed to list memories")?;
// We can't load individual memories without a load_memory method on the
// service. For now, list returns summary info; callers who need full
// content use a separate endpoint. Return minimal responses keyed by slug.
let responses: Vec<MemoryResponse> = slugs
.into_iter()
.map(|slug| MemoryResponse {
name: slug.clone(),
description: String::new(),
content: String::new(),
kind: String::new(),
created_at: 0,
updated_at: 0,
outcome: None,
lifecycle: String::new(),
scope: None,
before_snippet: None,
after_snippet: None,
provenances: Vec::new(),
})
.collect();
Ok(responses)
}
/// Handle `POST /memories`
///
/// Creates or updates a memory from the request body.
pub fn handle_create_memory<M: MemoryService>(
service: &M,
req: MemoryCreateRequest,
) -> Result<MemoryResponse> {
let now = chrono::Utc::now().timestamp();
let memory = Memory {
name: req.name,
description: req.description,
content: req.content,
kind: req.kind.unwrap_or_else(|| "reference".to_string()),
created_at: now,
updated_at: now,
outcome: req.outcome,
lifecycle: req.lifecycle.unwrap_or_else(|| "new".to_string()),
scope: req.scope,
before_snippet: req.before_snippet,
after_snippet: req.after_snippet,
provenances: req.provenances.unwrap_or_default(),
};
service
.save_memory(&memory)
.context("failed to save memory")?;
Ok(MemoryResponse::from(memory))
}