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
+9
View File
@@ -0,0 +1,9 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! Re-exports from zesdex-entities crate for canonical ChatMessage/Role types.
pub use zesdex_entities::seaorm::common::message::*;
+11
View File
@@ -0,0 +1,11 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! Chat DTO submodules: message roles/content and tool-call structures.
pub mod message;
pub mod tool;
+10
View File
@@ -0,0 +1,10 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! Re-exports from zesdex-entities crate for canonical ToolCall/ToolResult types.
pub use zesdex_entities::seaorm::common::tool_call::*;
pub use zesdex_entities::seaorm::common::tool_result::*;
+16
View File
@@ -0,0 +1,16 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! Data transfer objects for LLM provider API communication and chat message
//! wire formats.
//!
//! Sub-modules:
//! - [`chat`] — `ChatMessage`, `Role`, `ToolCall`, `ToolResult`
//! - [`provider`] — `ChatCompletionRequest`, `ChatCompletionResponse`, `TokenUsage`
pub mod chat;
pub mod provider;
+12
View File
@@ -0,0 +1,12 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! Provider-facing DTOs: chat completion request, response, and usage/cost.
pub mod request;
pub mod response;
pub mod usage;
+81
View File
@@ -0,0 +1,81 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! Outbound request DTOs for the OpenAI/Anthropic-compatible chat completions API.
//!
//! Flow: harness/runtime builds a [`ChatCompletionRequest`] from conversation
//! state and the active tool set → serializes to JSON via `serde` → sends to
//! the provider's `/chat/completions`-style endpoint (streaming or not).
//!
//! Why: fields mirror the wire format exactly (including `#[serde(rename)]`
//! for reserved words like `type`) so no manual (de)serialization glue is
//! needed; optional fields use `skip_serializing_if` so unset knobs are
//! omitted rather than sent as `null`, matching provider expectations.
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Outbound chat completion request body sent to an
/// OpenAI/Anthropic-compatible provider.
///
/// Flow: constructed from the current message history plus optional
/// generation knobs (temperature, `max_tokens`, tools, etc.) and serialized
/// directly into the HTTP request body.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatCompletionRequest {
pub model: String,
pub messages: Vec<super::super::chat::message::ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_options: Option<StreamOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<ToolDef>>,
/// Controls which (if any) function is called by the model.
/// Can be `"none"`, `"auto"`, or `{"type": "function", "function": {"name": "..."}}`.
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop: Option<Vec<String>>,
}
/// Streaming options for the request; `include_usage` asks the provider to
/// emit a final usage chunk in the SSE stream.
///
/// Why: usage tokens are otherwise unavailable in a streamed response since
/// they are normally only attached to the final non-streamed completion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamOptions {
pub include_usage: bool,
}
/// Wire format for a single tool definition sent to the provider.
///
/// Flow: built from the harness's registered `Tool` implementations
/// and attached to [`ChatCompletionRequest::tools`] so the model knows which
/// functions it may call.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDef {
#[serde(rename = "type")]
pub type_: String,
pub function: ToolFunctionDef,
}
/// Name, description, and JSON schema parameters for a tool definition.
///
/// Why: `parameters` is a raw `serde_json::Value` rather than a typed struct
/// because each tool defines its own arbitrary JSON schema.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolFunctionDef {
pub name: String,
pub description: String,
pub parameters: Value,
}
@@ -0,0 +1,58 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! Inbound response DTOs for the non-streaming chat completions API.
//!
//! Flow: provider HTTP response body → `serde_json` deserializes into
//! [`ChatCompletionResponse`] → caller reads `choices[0]` for the assistant
//! reply and `usage` for token accounting.
use serde::{Deserialize, Serialize};
/// Non-streaming chat completion response returned by the provider.
///
/// Flow: deserialized directly from the HTTP response body of a
/// non-streaming completion call.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatCompletionResponse {
pub id: String,
pub object: String,
pub created: i64,
pub model: String,
pub choices: Vec<Choice>,
pub usage: Option<super::usage::TokenUsage>,
}
/// One completion candidate within a [`ChatCompletionResponse::choices`] list.
///
/// For non-streaming responses the `message` field is populated; for streaming
/// responses the `delta` field carries the incremental token.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Choice {
pub index: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<super::super::chat::message::ChatMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta: Option<Delta>,
#[serde(skip_serializing_if = "Option::is_none")]
pub finish_reason: Option<String>,
}
/// Incremental delta emitted in a streaming SSE chunk.
///
/// Only populated when the response is streamed; `role` typically appears
/// only on the first chunk and `content` / `tool_calls` are appended
/// incrementally.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delta {
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<super::super::chat::message::Role>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<super::super::chat::tool::ToolCall>>,
}
+28
View File
@@ -0,0 +1,28 @@
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
//! Token usage accounting DTO shared by streaming and non-streaming responses.
//!
//! Flow: populated from the provider's `usage` object (either the final SSE
//! chunk when `stream_options.include_usage` is set, or the `usage` field of
//! a non-streaming [`ChatCompletionResponse`](super::response::ChatCompletionResponse))
//! → surfaced to the TUI for cost/token display.
use serde::{Deserialize, Serialize};
/// Token counts for a single completion request.
///
/// Why: these are the standard fields reported by the OpenAI-compatible chat
/// completions API. All fields are required when present — use `Option` at
/// the [`ChatCompletionResponse`](super::response::ChatCompletionResponse) level
/// if usage is absent.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct TokenUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}