use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ChangeKind { Added, Modified, Deleted, Renamed, } impl Default for ChangeKind { fn default() -> Self { ChangeKind::Modified } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CodeChange { pub timestamp: u64, pub file_path: String, pub description: String, #[serde(default)] pub git_commit: Option, #[serde(default)] pub git_branch: Option, #[serde(default)] pub repo_name: Option, #[serde(default)] pub repo_url: Option, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub change_kind: ChangeKind, #[serde(default)] pub symbols: Vec, #[serde(default)] pub line_range: Option, #[serde(default)] pub author: Option, #[serde(default)] pub session_id: Option, #[serde(default)] pub vcs_type: Option, #[serde(default)] pub revision: Option, #[serde(default)] pub branch: Option, #[serde(default)] pub repository_root: Option, } impl CodeChange { pub fn effective_vcs(&self) -> &str { if let Some(vcs) = &self.vcs_type { return vcs.as_str(); } if let Some(rev) = self.revision.as_deref().or(self.git_commit.as_deref()) { if rev.starts_with('r') && rev[1..].chars().all(|c| c.is_ascii_digit()) { return "svn"; } } if let Some(br) = self.branch.as_deref().or(self.git_branch.as_deref()) { if br.eq_ignore_ascii_case("trunk") || br.starts_with("branches/") || br.starts_with("tags/") { return "svn"; } } "git" } pub fn effective_revision(&self) -> Option<&str> { self.revision.as_deref().or(self.git_commit.as_deref()) } pub fn effective_branch(&self) -> Option<&str> { self.branch.as_deref().or(self.git_branch.as_deref()) } } pub fn generate_uuid() -> String { uuid::Uuid::new_v4().to_string() } pub fn default_namespace() -> String { "global".to_string() } pub fn normalize_entity_type(input: &str) -> String { let trimmed = input.trim(); if trimmed.is_empty() { return "Concept".to_string(); } let clean = trimmed.replace(['_', '-'], " "); let words: Vec<&str> = clean.split_whitespace().collect(); if words.is_empty() { return "Concept".to_string(); } let pascal: String = words .into_iter() .map(|w| { let mut chars = w.chars(); match chars.next() { None => String::new(), Some(f) => f.to_uppercase().collect::() + &chars.as_str().to_lowercase(), } }) .collect(); match pascal.as_str() { "File" => "File".to_string(), "McpTool" | "Mcptool" | "Mcp" => "McpTool".to_string(), "Databasetable" | "DatabaseTable" => "DatabaseTable".to_string(), "Datastructure" | "DataStore" | "Datastore" => "DataStructure".to_string(), "Architecturecomponent" | "ArchitectureComponent" | "Architecture" => { "ArchitectureComponent".to_string() } "Frontendcomponent" | "FrontendComponent" => "FrontendComponent".to_string(), "Globalrule" | "GlobalRule" => "GlobalRule".to_string(), "Securitypolicy" | "SecurityPolicy" => "SecurityPolicy".to_string(), "Serviceaccount" | "ServiceAccount" => "ServiceAccount".to_string(), "Testframework" | "TestFramework" => "TestFramework".to_string(), "Toolhandler" | "ToolHandler" => "ToolHandler".to_string(), "Historicalsummary" | "HistoricalSummary" => "HistoricalSummary".to_string(), "Externalapi" | "ExternalApi" => "ExternalAPI".to_string(), "Testsuite" | "TestSuite" => "TestSuite".to_string(), "Confluencepage" | "ConfluencePage" => "ConfluencePage".to_string(), "Containerimage" | "ContainerImage" => "ContainerImage".to_string(), "Buildsystem" | "BuildSystem" => "BuildSystem".to_string(), _ => pascal, } } pub fn normalize_relation_type(input: &str) -> String { let trimmed = input.trim(); if trimmed.is_empty() { return "related_to".to_string(); } trimmed.to_lowercase().replace([' ', '-'], "_") } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)] #[serde(rename_all = "snake_case")] pub enum TaskStatus { Pending, #[default] Active, InProgress, Blocked, Completed, Cancelled, } impl TaskStatus { pub fn is_active(&self) -> bool { matches!( self, TaskStatus::Pending | TaskStatus::Active | TaskStatus::InProgress | TaskStatus::Blocked ) } pub fn is_completed(&self) -> bool { matches!(self, TaskStatus::Completed) } pub fn as_str(&self) -> &'static str { match self { TaskStatus::Pending => "pending", TaskStatus::Active => "active", TaskStatus::InProgress => "in_progress", TaskStatus::Blocked => "blocked", TaskStatus::Completed => "completed", TaskStatus::Cancelled => "cancelled", } } } impl std::fmt::Display for TaskStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] pub struct Entity { pub name: String, #[serde(alias = "entityType", alias = "type", alias = "category")] pub entity_type: String, #[serde(default)] pub observations: Vec, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub git_branch: Option, #[serde(default)] pub repo_name: Option, #[serde(default)] pub file_path: Option, #[serde(default)] pub created_at: Option, #[serde(default)] pub updated_at: Option, #[serde(default)] pub embedding: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] pub struct Relation { #[serde(alias = "source", alias = "from_node", alias = "source_node")] pub from: String, #[serde(alias = "target", alias = "to_node", alias = "target_node")] pub to: String, #[serde(alias = "relationType", alias = "type", alias = "relation")] pub relation_type: String, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub repo_name: Option, #[serde(default)] pub weight: Option, #[serde(default)] pub is_bidirectional: bool, } impl PartialEq for Relation { fn eq(&self, other: &Self) -> bool { self.from == other.from && self.to == other.to && self.relation_type == other.relation_type && self.namespace == other.namespace && self.repo_name == other.repo_name } } impl Eq for Relation {} impl std::hash::Hash for Relation { fn hash(&self, state: &mut H) { self.from.hash(state); self.to.hash(state); self.relation_type.hash(state); self.namespace.hash(state); self.repo_name.hash(state); } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct KnowledgeGraph { #[serde(default)] pub entities: HashMap, #[serde(default)] pub relations: Vec, } impl KnowledgeGraph { /// Builds an adjacency index mapping node names to lists of outgoing/incoming neighbors and relation types. pub fn build_adjacency_map<'a>( &'a self, relation_filter: Option<&'a str>, ) -> HashMap<&'a str, Vec<(&'a str, &'a str, bool)>> { let mut adj: HashMap<&'a str, Vec<(&'a str, &'a str, bool)>> = HashMap::with_capacity(self.relations.len() * 2); for rel in &self.relations { if let Some(rf) = relation_filter { if rel.relation_type != rf { continue; } } adj.entry(rel.from.as_str()).or_default().push(( rel.to.as_str(), rel.relation_type.as_str(), false, )); adj.entry(rel.to.as_str()).or_default().push(( rel.from.as_str(), rel.relation_type.as_str(), true, )); } adj } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AcceptanceCriteria { pub id: String, pub description: String, pub is_met: bool, #[serde(default)] pub verification_command: Option, #[serde(default)] pub verification_output: Option, #[serde(default)] pub verified_at: Option, #[serde(default)] pub verified_by: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Task { pub id: String, pub title: String, pub status: String, pub description: String, pub created_at: u64, pub updated_at: u64, pub git_branch: Option, #[serde(default)] pub parent_id: Option, #[serde(default)] pub expires_at: Option, #[serde(default)] pub dependencies: Vec, #[serde(default)] pub acceptance_criteria: Vec, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub repo_name: Option, #[serde(default)] pub priority: Option, #[serde(default)] pub assigned_agent: Option, #[serde(default)] pub verification_command: Option, } impl Task { pub fn is_active(&self) -> bool { let s = self.status.trim(); !s.eq_ignore_ascii_case("completed") && !s.eq_ignore_ascii_case("done") && !s.eq_ignore_ascii_case("cancelled") && !s.eq_ignore_ascii_case("canceled") } } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Snippet { pub name: String, pub language: String, pub code: String, pub description: String, pub updated_at: u64, #[serde(default)] pub tags: Vec, #[serde(default)] pub embedding: Option>, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub repo_name: Option, #[serde(default)] pub origin_file: Option, #[serde(default)] pub line_range: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Adr { pub id: String, pub title: String, pub context: String, pub decision: String, #[serde(alias = "consequences")] pub consequence: String, #[serde(default = "default_adr_status")] pub status: String, #[serde(default)] pub supersedes: Option, pub timestamp: u64, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub repo_name: Option, #[serde(default)] pub alternatives_considered: Vec, #[serde(default)] pub affected_components: Vec, #[serde(default)] pub author: Option, #[serde(default)] pub git_commit: Option, #[serde(default)] pub git_branch: Option, #[serde(default)] pub resolved_at: Option, #[serde(default)] pub task_id: Option, } fn default_adr_status() -> String { "accepted".to_string() } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ErrorFix { pub signature: String, pub solution: String, pub timestamp: u64, pub git_commit: Option, pub git_branch: Option, #[serde(default)] pub embedding: Option>, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub repo_name: Option, #[serde(default)] pub error_category: Option, #[serde(default)] pub stack_trace: Option, #[serde(default)] pub toolchain: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SessionSummary { pub timestamp: u64, pub summary: String, pub namespace: String, #[serde(default)] pub expires_at: Option, #[serde(default)] pub session_id: Option, #[serde(default)] pub repo_name: Option, #[serde(default)] pub git_branch: Option, #[serde(default)] pub git_commit: Option, #[serde(default)] pub tasks_completed: Vec, #[serde(default)] pub next_steps: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct HandoffMemo { pub id: String, pub author: String, pub content: String, #[serde(default)] pub expires_at: Option, pub namespace: String, pub timestamp: u64, #[serde(default)] pub session_id: Option, #[serde(default)] pub repo_name: Option, #[serde(default)] pub git_branch: Option, #[serde(default)] pub vcs_revision: Option, #[serde(default)] pub blockers: Vec, #[serde(default)] pub action_items: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct EnvFingerprint { pub namespace: String, pub os: String, pub shell: String, pub tool_versions: HashMap, pub updated_at: u64, #[serde(default)] pub repo_name: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct EnvRequirement { pub namespace: String, pub key: String, pub description: String, pub is_secret: bool, #[serde(default)] pub repo_name: Option, #[serde(default)] pub default_value: Option, #[serde(default)] pub validation_regex: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Milestone { pub id: String, pub title: String, pub status: String, pub namespace: String, pub target_date: Option, #[serde(default)] pub description: Option, #[serde(default)] pub repo_name: Option, #[serde(default)] pub task_ids: Vec, #[serde(default)] pub deliverables: Vec, #[serde(default)] pub completed_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct EnvironmentDetail { pub namespace: String, pub name: String, pub url: String, pub description: String, pub requires_vpn: bool, pub updated_at: u64, #[serde(default)] pub repo_name: Option, #[serde(default)] pub env_type: Option, #[serde(default)] pub healthcheck_endpoint: Option, #[serde(default)] pub ssh_host: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct TechDebt { pub id: String, pub namespace: String, pub description: String, pub ideal_solution: String, pub is_resolved: bool, pub created_at: u64, pub git_commit: Option, pub git_branch: Option, #[serde(default)] pub embedding: Option>, #[serde(default)] pub repo_name: Option, #[serde(default)] pub severity: Option, #[serde(default)] pub file_path: Option, #[serde(default)] pub line_range: Option, #[serde(default)] pub workaround: Option, #[serde(default)] pub effort_estimate: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct GateRecord { pub id: String, pub action: String, pub target: String, pub namespace: Option, pub params: std::collections::HashMap, pub status: String, pub reason: Option, pub timestamp: u64, #[serde(default)] pub repo_name: Option, #[serde(default)] pub gate_type: Option, #[serde(default)] pub enforcer: Option, #[serde(default)] pub validation_log: Option, #[serde(default)] pub expires_at: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct TerminalHistory { #[serde(default = "generate_uuid")] pub id: String, pub command: String, pub exit_code: i32, pub cwd: String, #[serde(default)] pub os: String, pub timestamp: u64, #[serde(default)] pub error_output: Option, #[serde(default)] pub shell: Option, #[serde(default)] pub duration_ms: Option, #[serde(default)] pub stdout_summary: Option, #[serde(default)] pub status_reason: Option, #[serde(default)] pub invoker: Option, #[serde(default)] pub repo_name: Option, #[serde(default = "default_namespace")] pub namespace: String, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ActivityRecord { pub timestamp: u64, pub category: String, // e.g. "code_change", "task_update", "telemetry", "error_fix" pub summary: String, // Brief human-readable description of what is being worked on pub details: Option, #[serde(default)] pub session_id: Option, #[serde(default)] pub actor: Option, #[serde(default)] pub repo_name: Option, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub associated_file: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct StateSnapshot { pub id: String, pub timestamp: u64, pub description: String, pub namespace: String, #[serde(default)] pub repo_name: Option, #[serde(default)] pub git_branch: Option, #[serde(default)] pub vcs_revision: Option, #[serde(default)] pub active_tasks_count: usize, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Hypothesis { pub id: String, pub task_id: Option, pub hypothesis: String, pub status: String, pub evidence: Option, pub timestamp: u64, #[serde(default = "default_namespace")] pub namespace: String, #[serde(default)] pub repo_name: Option, #[serde(default)] pub validation_steps: Vec, #[serde(default)] pub diagnostician: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AgentSignal { pub id: String, pub sender: String, pub signal_type: String, pub payload: String, pub timestamp: u64, pub ttl_seconds: Option, #[serde(default)] pub recipient: Option, #[serde(default)] pub correlation_id: Option, #[serde(default)] pub priority: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct TaskEvent { pub task_id: String, pub status: String, pub action: Option, pub result: Option, pub error: Option, pub timestamp: u64, pub session_id: Option, #[serde(default)] pub namespace: Option, #[serde(default)] pub repo_name: Option, #[serde(default)] pub actor: Option, } #[cfg(test)] mod tests { use crate::error::AppError; use axum::http::StatusCode; use axum::response::IntoResponse; #[test] fn test_app_error_bad_request_invalid_json() { let err = AppError::BadRequest("invalid json".into()); let res = err.into_response(); assert_eq!(res.status(), StatusCode::BAD_REQUEST); } }