Files
mcp-memory/server/src/models.rs
T

705 lines
20 KiB
Rust

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<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub repo_url: Option<String>,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub change_kind: ChangeKind,
#[serde(default)]
pub symbols: Vec<String>,
#[serde(default)]
pub line_range: Option<String>,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub vcs_type: Option<String>,
#[serde(default)]
pub revision: Option<String>,
#[serde(default)]
pub branch: Option<String>,
#[serde(default)]
pub repository_root: Option<String>,
}
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::<String>() + &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<String>,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub file_path: Option<String>,
#[serde(default)]
pub created_at: Option<u64>,
#[serde(default)]
pub updated_at: Option<u64>,
#[serde(default)]
pub embedding: Option<Vec<f32>>,
}
#[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<String>,
#[serde(default)]
pub weight: Option<f32>,
#[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<H: std::hash::Hasher>(&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<String, Entity>,
#[serde(default)]
pub relations: Vec<Relation>,
}
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<String>,
#[serde(default)]
pub verification_output: Option<String>,
#[serde(default)]
pub verified_at: Option<u64>,
#[serde(default)]
pub verified_by: Option<String>,
}
#[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<String>,
#[serde(default)]
pub parent_id: Option<String>,
#[serde(default)]
pub expires_at: Option<u64>,
#[serde(default)]
pub dependencies: Vec<String>,
#[serde(default)]
pub acceptance_criteria: Vec<AcceptanceCriteria>,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub priority: Option<String>,
#[serde(default)]
pub assigned_agent: Option<String>,
#[serde(default)]
pub verification_command: Option<String>,
}
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<String>,
#[serde(default)]
pub embedding: Option<Vec<f32>>,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub origin_file: Option<String>,
#[serde(default)]
pub line_range: Option<String>,
}
#[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<String>,
pub timestamp: u64,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub alternatives_considered: Vec<String>,
#[serde(default)]
pub affected_components: Vec<String>,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub git_commit: Option<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub resolved_at: Option<u64>,
#[serde(default)]
pub task_id: Option<String>,
}
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<String>,
pub git_branch: Option<String>,
#[serde(default)]
pub embedding: Option<Vec<f32>>,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub error_category: Option<String>,
#[serde(default)]
pub stack_trace: Option<String>,
#[serde(default)]
pub toolchain: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionSummary {
pub timestamp: u64,
pub summary: String,
pub namespace: String,
#[serde(default)]
pub expires_at: Option<u64>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub git_commit: Option<String>,
#[serde(default)]
pub tasks_completed: Vec<String>,
#[serde(default)]
pub next_steps: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HandoffMemo {
pub id: String,
pub author: String,
pub content: String,
#[serde(default)]
pub expires_at: Option<u64>,
pub namespace: String,
pub timestamp: u64,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub vcs_revision: Option<String>,
#[serde(default)]
pub blockers: Vec<String>,
#[serde(default)]
pub action_items: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnvFingerprint {
pub namespace: String,
pub os: String,
pub shell: String,
pub tool_versions: HashMap<String, String>,
pub updated_at: u64,
#[serde(default)]
pub repo_name: Option<String>,
}
#[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<String>,
#[serde(default)]
pub default_value: Option<String>,
#[serde(default)]
pub validation_regex: Option<String>,
}
#[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<u64>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub task_ids: Vec<String>,
#[serde(default)]
pub deliverables: Vec<String>,
#[serde(default)]
pub completed_at: Option<u64>,
}
#[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<String>,
#[serde(default)]
pub env_type: Option<String>,
#[serde(default)]
pub healthcheck_endpoint: Option<String>,
#[serde(default)]
pub ssh_host: Option<String>,
}
#[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<String>,
pub git_branch: Option<String>,
#[serde(default)]
pub embedding: Option<Vec<f32>>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub severity: Option<String>,
#[serde(default)]
pub file_path: Option<String>,
#[serde(default)]
pub line_range: Option<String>,
#[serde(default)]
pub workaround: Option<String>,
#[serde(default)]
pub effort_estimate: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GateRecord {
pub id: String,
pub action: String,
pub target: String,
pub namespace: Option<String>,
pub params: std::collections::HashMap<String, String>,
pub status: String,
pub reason: Option<String>,
pub timestamp: u64,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub gate_type: Option<String>,
#[serde(default)]
pub enforcer: Option<String>,
#[serde(default)]
pub validation_log: Option<String>,
#[serde(default)]
pub expires_at: Option<u64>,
}
#[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<String>,
#[serde(default)]
pub shell: Option<String>,
#[serde(default)]
pub duration_ms: Option<u64>,
#[serde(default)]
pub stdout_summary: Option<String>,
#[serde(default)]
pub status_reason: Option<String>,
#[serde(default)]
pub invoker: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[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<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub actor: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub associated_file: Option<String>,
}
#[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<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub vcs_revision: Option<String>,
#[serde(default)]
pub active_tasks_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Hypothesis {
pub id: String,
pub task_id: Option<String>,
pub hypothesis: String,
pub status: String,
pub evidence: Option<String>,
pub timestamp: u64,
#[serde(default = "default_namespace")]
pub namespace: String,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub validation_steps: Vec<String>,
#[serde(default)]
pub diagnostician: Option<String>,
}
#[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<u64>,
#[serde(default)]
pub recipient: Option<String>,
#[serde(default)]
pub correlation_id: Option<String>,
#[serde(default)]
pub priority: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TaskEvent {
pub task_id: String,
pub status: String,
pub action: Option<String>,
pub result: Option<serde_json::Value>,
pub error: Option<String>,
pub timestamp: u64,
pub session_id: Option<String>,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub repo_name: Option<String>,
#[serde(default)]
pub actor: Option<String>,
}
#[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);
}
}