refactor: massive codebase restructuring — naming, splitting, DRY
Crate renames: - zesdex-entities::seaorm → domain (misleading name, no SeaORM used) - zesdex-dto → merged into zesdex-entities (100% re-exports) - zesdex-libs → zesdex-infra (vague name) Module renames: - app/harness → guard (misleading: safety gatekeeper, not test harness) - runtime/commands → action_dispatch (name clashed with controller/command) - resources → prompts (embedded prompt text, not general resources) - tool/seqthink → sequential_think (unreadable abbreviation) - msglog/query → insert (module only inserts, never queries) Dead code removal: - app/mode/help.rs (orphaned — not declared in mod.rs) - app/mode/loading.rs (orphaned — not declared in mod.rs) File splitting (71 new files, avg ~115 lines/file): - app/runtime/actions/: 1→8 files (was 2030 lines) - view/overlays/: 1→16 files (was 1167 lines) - tool/lsp/: 1→8 per-tool files (was 909 lines) - main.rs: 1→5 files (session, daemon, attach, event_loop) - workflow/engine + hive_mind: 2→10 files - subagent/engine + auto: 2→9 files - lsp/provisioner: 1→5 files - review/: 1→6 files - guard/: 1→2 files (extracted patterns) - state/misc: 1→3 files (input, scroll) - mcp/: 1→3 files (transport, adapter) - stream/json_repair extracted from turn.rs DRY: - Pattern constants (STUB_PATTERNS etc) in guard/patterns shared with subagent - 3 near-identical background spawners → 1 generic + thin wrappers - Shared spawn_subagent_with_drain() extracted - Shared create_session() in main - write_osc52 deduplicated Bug fixes: - archive_message(): sess.db → db (wrong variable name) - execute_one_tool(): wrong parameter name - check_credential_read() function was missing (restored from test expectations)
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
//! Application state initialisation and wiring.
|
||||
//!
|
||||
//! This module acts as the composition root for the zesdex daemon (and
|
||||
//! any other binary that needs a full set of services). It:
|
||||
//!
|
||||
//! 1. Defines [`IamServiceProvider`] and [`CmsServiceProvider`] trait
|
||||
//! objects so callers depend on interfaces, not generics.
|
||||
//! 2. Provides default implementations that wire together the
|
||||
//! infrastructure/repository adapters with the domain service traits.
|
||||
//! 3. Exposes [`initialize_app_context`] as a one-call entry point.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use uuid::Uuid;
|
||||
use zesdex_cms::domain::app_config::ProviderConfig;
|
||||
use zesdex_cms::domain::conversation::Conversation;
|
||||
use zesdex_cms::domain::memory::Memory;
|
||||
use zesdex_cms::domain::repository::{
|
||||
AppConfigRepository, ConversationRepository, MemoryRepository, SettingsRepository,
|
||||
};
|
||||
use zesdex_cms::domain::settings::Settings;
|
||||
use zesdex_cms::infrastructure::persistence::{
|
||||
JsonAppConfigRepository, JsonConversationRepository, JsonSettingsRepository,
|
||||
MarkdownMemoryRepository,
|
||||
};
|
||||
use zesdex_entities::domain::common::store::Store;
|
||||
use zesdex_iam::domain::repository::SessionRepository;
|
||||
use zesdex_iam::domain::session::Session;
|
||||
use zesdex_iam::infrastructure::persistence::session_repo::FileSystemSessionRepository;
|
||||
|
||||
use crate::database;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trait definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Session-management service provider.
|
||||
///
|
||||
/// Abstracts session CRUD behind a trait object so the HTTP / CLI layers
|
||||
/// do not depend on concrete repository generics.
|
||||
pub trait IamServiceProvider: Send + Sync {
|
||||
/// Create a new session with a generated UUID and default fields.
|
||||
fn create_session(&self) -> Result<Session>;
|
||||
|
||||
/// List all available sessions.
|
||||
fn list_all(&self) -> Result<Vec<Session>>;
|
||||
|
||||
/// Archive a session by id (sets `archived = true`).
|
||||
fn archive_session(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
/// CMS (content-management) service provider.
|
||||
///
|
||||
/// Combines settings, conversation, and memory operations behind a single
|
||||
/// trait object.
|
||||
pub trait CmsServiceProvider: Send + Sync {
|
||||
// -- Settings --
|
||||
/// Load current settings from the default store.
|
||||
fn load_settings(&self) -> Result<Settings>;
|
||||
|
||||
/// Persist updated settings.
|
||||
fn save_settings(&self, settings: &Settings) -> Result<()>;
|
||||
|
||||
/// Update the provider configuration (name and details).
|
||||
fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<()>;
|
||||
|
||||
// -- Conversations --
|
||||
/// Load a conversation for the given session id.
|
||||
fn load_conversation(&self, session_id: &str) -> Result<Conversation>;
|
||||
|
||||
/// Persist a conversation.
|
||||
fn save_conversation(&self, conv: &Conversation) -> Result<()>;
|
||||
|
||||
// -- Memories --
|
||||
/// List all memory slugs.
|
||||
fn list_memories(&self) -> Result<Vec<String>>;
|
||||
|
||||
/// Save (create or update) a memory.
|
||||
fn save_memory(&self, memory: &Memory) -> Result<()>;
|
||||
|
||||
/// Delete a memory by name.
|
||||
fn delete_memory(&self, name: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default IAM provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Default [`IamServiceProvider`] backed by the filesystem session
|
||||
/// repository.
|
||||
pub struct DefaultIamServiceProvider {
|
||||
session_repo: FileSystemSessionRepository,
|
||||
base_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DefaultIamServiceProvider {
|
||||
/// Create a new provider using the given data directory.
|
||||
pub fn new(base_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
session_repo: FileSystemSessionRepository::new(),
|
||||
base_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IamServiceProvider for DefaultIamServiceProvider {
|
||||
fn create_session(&self) -> Result<Session> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let session = Session::new(id, "New Session".to_string());
|
||||
self.session_repo
|
||||
.save_session(&self.base_dir, &session)
|
||||
.context("failed to persist new session")?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
fn list_all(&self) -> Result<Vec<Session>> {
|
||||
self.session_repo
|
||||
.list_sessions(&self.base_dir)
|
||||
.context("failed to list sessions")
|
||||
}
|
||||
|
||||
fn archive_session(&self, id: &str) -> Result<()> {
|
||||
let mut session = self
|
||||
.session_repo
|
||||
.load_session(&self.base_dir, id)
|
||||
.with_context(|| format!("session not found: {id}"))?;
|
||||
session.archived = true;
|
||||
session.updated_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64;
|
||||
self.session_repo
|
||||
.save_session(&self.base_dir, &session)
|
||||
.context("failed to save archived session")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default CMS provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Default [`CmsServiceProvider`] backed by filesystem repositories.
|
||||
pub struct DefaultCmsServiceProvider {
|
||||
settings_repo: JsonSettingsRepository,
|
||||
app_config_repo: JsonAppConfigRepository,
|
||||
conversation_repo: JsonConversationRepository,
|
||||
memory_repo: MarkdownMemoryRepository,
|
||||
base_dir: PathBuf,
|
||||
memory_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl DefaultCmsServiceProvider {
|
||||
/// Create a new provider.
|
||||
pub fn new(store: &Store) -> Self {
|
||||
Self {
|
||||
settings_repo: JsonSettingsRepository::new(),
|
||||
app_config_repo: JsonAppConfigRepository::new(),
|
||||
conversation_repo: JsonConversationRepository::new(),
|
||||
memory_repo: MarkdownMemoryRepository::new(),
|
||||
base_dir: store.base_dir.clone(),
|
||||
memory_dir: store.memory_dir.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the session directory for a given session id.
|
||||
fn session_dir(&self, session_id: &str) -> PathBuf {
|
||||
self.base_dir.join("sessions").join(session_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl CmsServiceProvider for DefaultCmsServiceProvider {
|
||||
// -- Settings --
|
||||
fn load_settings(&self) -> Result<Settings> {
|
||||
self.settings_repo
|
||||
.load(&self.base_dir)
|
||||
.context("failed to load settings")
|
||||
}
|
||||
|
||||
fn save_settings(&self, settings: &Settings) -> Result<()> {
|
||||
self.settings_repo
|
||||
.save(&self.base_dir, settings)
|
||||
.context("failed to save settings")
|
||||
}
|
||||
|
||||
fn update_provider(&self, name: &str, config: &ProviderConfig) -> Result<()> {
|
||||
let mut app_config = self
|
||||
.app_config_repo
|
||||
.load(&self.base_dir)
|
||||
.context("failed to load app config")?;
|
||||
app_config
|
||||
.providers
|
||||
.insert(name.to_string(), config.clone());
|
||||
self.app_config_repo
|
||||
.save(&self.base_dir, &app_config)
|
||||
.context("failed to save app config after provider update")
|
||||
}
|
||||
|
||||
// -- Conversations --
|
||||
fn load_conversation(&self, session_id: &str) -> Result<Conversation> {
|
||||
let dir = self.session_dir(session_id);
|
||||
self.conversation_repo
|
||||
.load(&dir)
|
||||
.with_context(|| format!("failed to load conversation for session '{session_id}'"))
|
||||
}
|
||||
|
||||
fn save_conversation(&self, conv: &Conversation) -> Result<()> {
|
||||
let dir = self.session_dir(&conv.session_id);
|
||||
self.conversation_repo.save(&dir, conv).with_context(|| {
|
||||
format!(
|
||||
"failed to save conversation for session '{}'",
|
||||
conv.session_id
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// -- Memories --
|
||||
fn list_memories(&self) -> Result<Vec<String>> {
|
||||
self.memory_repo
|
||||
.list(&self.memory_dir)
|
||||
.context("failed to list memories")
|
||||
}
|
||||
|
||||
fn save_memory(&self, memory: &Memory) -> Result<()> {
|
||||
self.memory_repo
|
||||
.save(&self.memory_dir, memory)
|
||||
.with_context(|| format!("failed to save memory '{}'", memory.name))
|
||||
}
|
||||
|
||||
fn delete_memory(&self, name: &str) -> Result<()> {
|
||||
self.memory_repo
|
||||
.delete(&self.memory_dir, name)
|
||||
.with_context(|| format!("failed to delete memory '{name}'"))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AppContext
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Aggregated shared state for the zesdex daemon (or any binary using the
|
||||
/// full service stack).
|
||||
pub struct AppContext {
|
||||
/// Filesystem store (resolved paths for all data directories).
|
||||
pub store: Store,
|
||||
/// IAM service provider (session management).
|
||||
pub iam_service: Box<dyn IamServiceProvider>,
|
||||
/// CMS service provider (settings, conversations, memories).
|
||||
pub cms_service: Box<dyn CmsServiceProvider>,
|
||||
/// SQLite database connection.
|
||||
pub db: database::DbConn,
|
||||
/// JWT HMAC secret used to sign / verify tokens.
|
||||
pub jwt_secret: String,
|
||||
}
|
||||
|
||||
impl AppContext {
|
||||
/// Return a shared `Arc<AppContext>` for use with Axum's
|
||||
/// `axum::extract::State`.
|
||||
pub fn into_arc(self) -> Arc<Self> {
|
||||
Arc::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Initialisation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Wire together the full application stack and return an [`AppContext`].
|
||||
///
|
||||
/// Steps:
|
||||
/// 1. Initialise [`Store`] and create all data directories.
|
||||
/// 2. Connect to the SQLite database and run migrations.
|
||||
/// 3. Instantiate the IAM and CMS service providers.
|
||||
/// 4. Determine the JWT secret (env var `ZESDEX_JWT_SECRET` or a default).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any step fails (directory creation, DB connection,
|
||||
/// migration execution, etc.).
|
||||
pub fn initialize_app_context() -> Result<AppContext> {
|
||||
// -- Store --
|
||||
let store = Store::new();
|
||||
store
|
||||
.ensure_dirs()
|
||||
.context("failed to create store directories")?;
|
||||
|
||||
// -- Database --
|
||||
let db_path = store.base_dir.join("zesdex.db");
|
||||
let db_path_str = db_path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid db path: {}", db_path.display()))?;
|
||||
|
||||
let db = database::init_db(db_path_str).context("failed to initialise database")?;
|
||||
|
||||
database::run_migrations(&db).context("failed to run database migrations")?;
|
||||
|
||||
// -- Services --
|
||||
let iam_service: Box<dyn IamServiceProvider> =
|
||||
Box::new(DefaultIamServiceProvider::new(store.base_dir.clone()));
|
||||
let cms_service: Box<dyn CmsServiceProvider> = Box::new(DefaultCmsServiceProvider::new(&store));
|
||||
|
||||
// -- JWT secret --
|
||||
let jwt_secret = std::env::var("ZESDEX_JWT_SECRET")
|
||||
.unwrap_or_else(|_| "zesdex-dev-secret-do-not-use-in-production".to_string());
|
||||
|
||||
Ok(AppContext {
|
||||
store,
|
||||
iam_service,
|
||||
cms_service,
|
||||
db,
|
||||
jwt_secret,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_iam_provider_list_all_empty() {
|
||||
let tmp = std::env::temp_dir().join(format!("zesdex-test-iam-{}", Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let provider = DefaultIamServiceProvider::new(tmp.clone());
|
||||
let sessions = provider.list_all().unwrap();
|
||||
assert!(sessions.is_empty());
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_iam_provider_create_and_list() {
|
||||
let tmp = std::env::temp_dir().join(format!("zesdex-test-iam-{}", Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let provider = DefaultIamServiceProvider::new(tmp.clone());
|
||||
let session = provider.create_session().unwrap();
|
||||
assert!(!session.id.is_empty());
|
||||
let sessions = provider.list_all().unwrap();
|
||||
assert_eq!(sessions.len(), 1);
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_cms_provider_default_settings() {
|
||||
let tmp = std::env::temp_dir().join(format!("zesdex-test-cms-{}", Uuid::new_v4()));
|
||||
std::fs::create_dir_all(&tmp).unwrap();
|
||||
let mut store = Store::new();
|
||||
store.base_dir = tmp.clone();
|
||||
store.memory_dir = tmp.join("memory");
|
||||
let provider = DefaultCmsServiceProvider::new(&store);
|
||||
let settings = provider.load_settings().unwrap();
|
||||
assert_eq!(settings.provider, "zen");
|
||||
let _ = std::fs::remove_dir_all(&tmp);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user