use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};
fn null_to_empty<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
Option::<String>::deserialize(d).map(|o| o.unwrap_or_default())
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PatternType {
RepetitiveInstruction,
RecurringMistake,
WorkflowPattern,
StaleContext,
RedundantContext,
}
impl std::fmt::Display for PatternType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RepetitiveInstruction => write!(f, "repetitive_instruction"),
Self::RecurringMistake => write!(f, "recurring_mistake"),
Self::WorkflowPattern => write!(f, "workflow_pattern"),
Self::StaleContext => write!(f, "stale_context"),
Self::RedundantContext => write!(f, "redundant_context"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PatternStatus {
Discovered,
Active,
Archived,
Dismissed,
}
impl std::fmt::Display for PatternStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Discovered => write!(f, "discovered"),
Self::Active => write!(f, "active"),
Self::Archived => write!(f, "archived"),
Self::Dismissed => write!(f, "dismissed"),
}
}
}
impl PatternStatus {
pub fn from_str(s: &str) -> Self {
match s {
"discovered" => Self::Discovered,
"active" => Self::Active,
"archived" => Self::Archived,
"dismissed" => Self::Dismissed,
_ => Self::Discovered,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SuggestedTarget {
Skill,
ClaudeMd,
GlobalAgent,
DbOnly,
}
impl std::fmt::Display for SuggestedTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Skill => write!(f, "skill"),
Self::ClaudeMd => write!(f, "claude_md"),
Self::GlobalAgent => write!(f, "global_agent"),
Self::DbOnly => write!(f, "db_only"),
}
}
}
impl SuggestedTarget {
pub fn from_str(s: &str) -> Self {
match s {
"skill" => Self::Skill,
"claude_md" => Self::ClaudeMd,
"global_agent" => Self::GlobalAgent,
"db_only" => Self::DbOnly,
_ => Self::DbOnly,
}
}
}
impl PatternType {
pub fn from_str(s: &str) -> Self {
match s {
"repetitive_instruction" => Self::RepetitiveInstruction,
"recurring_mistake" => Self::RecurringMistake,
"workflow_pattern" => Self::WorkflowPattern,
"stale_context" => Self::StaleContext,
"redundant_context" => Self::RedundantContext,
_ => Self::WorkflowPattern,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pattern {
pub id: String,
pub pattern_type: PatternType,
pub description: String,
pub confidence: f64,
pub times_seen: i64,
pub first_seen: DateTime<Utc>,
pub last_seen: DateTime<Utc>,
pub last_projected: Option<DateTime<Utc>>,
pub status: PatternStatus,
pub source_sessions: Vec<String>,
pub related_files: Vec<String>,
pub suggested_content: String,
pub suggested_target: SuggestedTarget,
pub project: Option<String>,
pub generation_failed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SessionEntry {
#[serde(rename = "user")]
User(UserEntry),
#[serde(rename = "assistant")]
Assistant(AssistantEntry),
#[serde(rename = "summary")]
Summary(SummaryEntry),
#[serde(rename = "file-history-snapshot")]
FileHistorySnapshot(serde_json::Value),
#[serde(rename = "progress")]
Progress(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserEntry {
pub uuid: String,
#[serde(default)]
pub parent_uuid: Option<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub timestamp: Option<String>,
pub message: UserMessage,
#[serde(default)]
pub is_sidechain: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserMessage {
pub role: String,
pub content: MessageContent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MessageContent {
Text(String),
Blocks(Vec<ContentBlock>),
Other(serde_json::Value),
}
impl MessageContent {
pub fn as_text(&self) -> String {
match self {
MessageContent::Text(s) => s.clone(),
MessageContent::Blocks(blocks) => {
let mut parts = Vec::new();
for block in blocks {
match block {
ContentBlock::Text { text } => parts.push(text.clone()),
ContentBlock::ToolResult { content, .. } => {
if let Some(c) = content {
parts.push(c.as_text());
}
}
_ => {}
}
}
parts.join("\n")
}
MessageContent::Other(_) => String::new(),
}
}
pub fn is_tool_result(&self) -> bool {
matches!(self, MessageContent::Blocks(blocks) if blocks.iter().any(|b| matches!(b, ContentBlock::ToolResult { .. })))
}
pub fn is_unknown(&self) -> bool {
matches!(self, MessageContent::Other(_))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ContentBlock {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "thinking")]
Thinking {
thinking: String,
#[serde(default)]
signature: Option<String>,
},
#[serde(rename = "tool_use")]
ToolUse {
id: String,
name: String,
#[serde(default)]
input: serde_json::Value,
},
#[serde(rename = "tool_result")]
ToolResult {
tool_use_id: String,
#[serde(default)]
content: Option<ToolResultContent>,
},
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolResultContent {
Text(String),
Blocks(Vec<serde_json::Value>),
}
impl ToolResultContent {
pub fn as_text(&self) -> String {
match self {
Self::Text(s) => s.clone(),
Self::Blocks(blocks) => {
blocks
.iter()
.filter_map(|b| b.get("text").and_then(|t| t.as_str()))
.collect::<Vec<_>>()
.join("\n")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssistantEntry {
pub uuid: String,
#[serde(default)]
pub parent_uuid: Option<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub timestamp: Option<String>,
pub message: AssistantMessage,
#[serde(default)]
pub is_sidechain: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssistantMessage {
pub role: String,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub content: Vec<ContentBlock>,
#[serde(default)]
pub stop_reason: Option<String>,
#[serde(default)]
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usage {
#[serde(default)]
pub input_tokens: u64,
#[serde(default)]
pub output_tokens: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SummaryEntry {
#[serde(default)]
pub uuid: String,
#[serde(default)]
pub parent_uuid: Option<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub timestamp: Option<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub message: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub session_id: String,
pub project: String,
pub session_path: String,
pub user_messages: Vec<ParsedUserMessage>,
pub assistant_messages: Vec<ParsedAssistantMessage>,
pub summaries: Vec<String>,
pub tools_used: Vec<String>,
pub errors: Vec<String>,
pub metadata: SessionMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsedUserMessage {
pub text: String,
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsedAssistantMessage {
pub text: String,
pub thinking_summary: Option<String>,
pub tools: Vec<String>,
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMetadata {
pub cwd: Option<String>,
pub version: Option<String>,
pub git_branch: Option<String>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HistoryEntry {
#[serde(default)]
pub display: Option<String>,
#[serde(default)]
pub timestamp: Option<u64>,
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginSkillSummary {
pub plugin_name: String,
pub skill_name: String,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextSnapshot {
pub claude_md: Option<String>,
pub skills: Vec<SkillFile>,
pub memory_md: Option<String>,
pub global_agents: Vec<AgentFile>,
#[serde(default)]
pub plugin_skills: Vec<PluginSkillSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillFile {
pub path: String,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentFile {
pub path: String,
pub content: String,
}
#[derive(Debug, Clone)]
pub struct IngestedSession {
pub session_id: String,
pub project: String,
pub session_path: String,
pub file_size: u64,
pub file_mtime: String,
pub ingested_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action")]
pub enum PatternUpdate {
#[serde(rename = "new")]
New(NewPattern),
#[serde(rename = "update")]
Update(UpdateExisting),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewPattern {
pub pattern_type: PatternType,
#[serde(deserialize_with = "null_to_empty")]
pub description: String,
pub confidence: f64,
#[serde(default)]
pub source_sessions: Vec<String>,
#[serde(default)]
pub related_files: Vec<String>,
#[serde(default, deserialize_with = "null_to_empty")]
pub suggested_content: String,
pub suggested_target: SuggestedTarget,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateExisting {
#[serde(deserialize_with = "null_to_empty")]
pub existing_id: String,
#[serde(default)]
pub new_sessions: Vec<String>,
pub new_confidence: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisResponse {
#[serde(default)]
pub reasoning: String,
pub patterns: Vec<PatternUpdate>,
#[serde(default)]
pub claude_md_edits: Vec<ClaudeMdEdit>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ClaudeMdEditType {
Add,
Remove,
Reword,
Move,
}
impl std::fmt::Display for ClaudeMdEditType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Add => write!(f, "add"),
Self::Remove => write!(f, "remove"),
Self::Reword => write!(f, "reword"),
Self::Move => write!(f, "move"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeMdEdit {
pub edit_type: ClaudeMdEditType,
#[serde(default)]
pub original_text: String,
#[serde(default)]
pub suggested_content: Option<String>,
#[serde(default)]
pub target_section: Option<String>,
#[serde(default)]
pub reasoning: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeCliOutput {
#[serde(default)]
pub result: Option<String>,
#[serde(default)]
pub is_error: bool,
#[serde(default)]
pub duration_ms: u64,
#[serde(default)]
pub num_turns: u64,
#[serde(default)]
pub stop_reason: Option<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub usage: Option<CliUsage>,
#[serde(default)]
pub structured_output: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CliUsage {
#[serde(default)]
pub input_tokens: u64,
#[serde(default)]
pub output_tokens: u64,
#[serde(default)]
pub cache_creation_input_tokens: u64,
#[serde(default)]
pub cache_read_input_tokens: u64,
}
impl ClaudeCliOutput {
pub fn total_input_tokens(&self) -> u64 {
self.usage.as_ref().map_or(0, |u| {
u.input_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens
})
}
pub fn total_output_tokens(&self) -> u64 {
self.usage.as_ref().map_or(0, |u| u.output_tokens)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub timestamp: DateTime<Utc>,
pub action: String,
pub details: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct BatchDetail {
pub batch_index: usize,
pub session_count: usize,
pub session_ids: Vec<String>,
pub prompt_chars: usize,
pub input_tokens: u64,
pub output_tokens: u64,
pub new_patterns: usize,
pub updated_patterns: usize,
pub reasoning: String,
pub ai_response_preview: String,
}
#[derive(Debug, Clone)]
pub struct AnalyzeResult {
pub sessions_analyzed: usize,
pub new_patterns: usize,
pub updated_patterns: usize,
pub total_patterns: usize,
pub input_tokens: u64,
pub output_tokens: u64,
pub batch_details: Vec<BatchDetail>,
}
#[derive(Debug, Clone)]
pub struct AnalyzeV2Result {
pub sessions_analyzed: usize,
pub nodes_created: usize,
pub nodes_updated: usize,
pub edges_created: usize,
pub nodes_merged: usize,
pub input_tokens: u64,
pub output_tokens: u64,
pub batch_count: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct CompactSession {
pub session_id: String,
pub project: String,
pub user_messages: Vec<CompactUserMessage>,
pub tools_used: Vec<String>,
pub errors: Vec<String>,
pub thinking_highlights: Vec<String>,
pub summaries: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CompactUserMessage {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CompactPattern {
pub id: String,
pub pattern_type: String,
pub description: String,
pub confidence: f64,
pub times_seen: i64,
pub suggested_target: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Projection {
pub id: String,
pub pattern_id: String,
pub target_type: String,
pub target_path: String,
pub content: String,
pub applied_at: DateTime<Utc>,
pub pr_url: Option<String>,
pub status: ProjectionStatus,
}
#[derive(Debug, Clone)]
pub struct SkillDraft {
pub name: String,
pub content: String,
pub pattern_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillValidation {
pub valid: bool,
#[serde(default)]
pub feedback: String,
}
#[derive(Debug, Clone)]
pub struct AgentDraft {
pub name: String,
pub content: String,
pub pattern_id: String,
}
#[derive(Debug, Clone)]
pub struct ApplyAction {
pub pattern_id: String,
pub pattern_description: String,
pub target_type: SuggestedTarget,
pub target_path: String,
pub content: String,
pub track: ApplyTrack,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProjectionStatus {
PendingReview,
Applied,
Dismissed,
}
impl std::fmt::Display for ProjectionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PendingReview => write!(f, "pending_review"),
Self::Applied => write!(f, "applied"),
Self::Dismissed => write!(f, "dismissed"),
}
}
}
impl ProjectionStatus {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"pending_review" => Some(Self::PendingReview),
"applied" => Some(Self::Applied),
"dismissed" => Some(Self::Dismissed),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ApplyTrack {
Personal,
Shared,
}
impl std::fmt::Display for ApplyTrack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Personal => write!(f, "personal"),
Self::Shared => write!(f, "shared"),
}
}
}
#[derive(Debug, Clone)]
pub struct ApplyPlan {
pub actions: Vec<ApplyAction>,
}
impl ApplyPlan {
pub fn is_empty(&self) -> bool {
self.actions.is_empty()
}
pub fn personal_actions(&self) -> Vec<&ApplyAction> {
self.actions.iter().filter(|a| a.track == ApplyTrack::Personal).collect()
}
pub fn shared_actions(&self) -> Vec<&ApplyAction> {
self.actions.iter().filter(|a| a.track == ApplyTrack::Shared).collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum NodeType {
Preference,
Pattern,
Rule,
Skill,
Memory,
Directive,
}
impl std::fmt::Display for NodeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Preference => write!(f, "preference"),
Self::Pattern => write!(f, "pattern"),
Self::Rule => write!(f, "rule"),
Self::Skill => write!(f, "skill"),
Self::Memory => write!(f, "memory"),
Self::Directive => write!(f, "directive"),
}
}
}
impl NodeType {
pub fn from_str(s: &str) -> Self {
match s {
"preference" => Self::Preference,
"pattern" => Self::Pattern,
"rule" => Self::Rule,
"skill" => Self::Skill,
"memory" => Self::Memory,
"directive" => Self::Directive,
_ => Self::Pattern,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum NodeScope {
Global,
Project,
}
impl std::fmt::Display for NodeScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Global => write!(f, "global"),
Self::Project => write!(f, "project"),
}
}
}
impl NodeScope {
pub fn from_str(s: &str) -> Self {
match s {
"global" => Self::Global,
_ => Self::Project,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum NodeStatus {
Active,
PendingReview,
Dismissed,
Archived,
}
impl std::fmt::Display for NodeStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Active => write!(f, "active"),
Self::PendingReview => write!(f, "pending_review"),
Self::Dismissed => write!(f, "dismissed"),
Self::Archived => write!(f, "archived"),
}
}
}
impl NodeStatus {
pub fn from_str(s: &str) -> Self {
match s {
"active" => Self::Active,
"pending_review" => Self::PendingReview,
"dismissed" => Self::Dismissed,
"archived" => Self::Archived,
_ => Self::Active,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum EdgeType {
Supports,
Contradicts,
Supersedes,
DerivedFrom,
AppliesTo,
}
impl std::fmt::Display for EdgeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Supports => write!(f, "supports"),
Self::Contradicts => write!(f, "contradicts"),
Self::Supersedes => write!(f, "supersedes"),
Self::DerivedFrom => write!(f, "derived_from"),
Self::AppliesTo => write!(f, "applies_to"),
}
}
}
impl EdgeType {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"supports" => Some(Self::Supports),
"contradicts" => Some(Self::Contradicts),
"supersedes" => Some(Self::Supersedes),
"derived_from" => Some(Self::DerivedFrom),
"applies_to" => Some(Self::AppliesTo),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeNode {
pub id: String,
pub node_type: NodeType,
pub scope: NodeScope,
pub project_id: Option<String>,
pub content: String,
pub confidence: f64,
pub status: NodeStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub projected_at: Option<String>,
#[serde(default)]
pub pr_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeEdge {
pub source_id: String,
pub target_id: String,
pub edge_type: EdgeType,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeProject {
pub id: String,
pub path: String,
pub remote_url: Option<String>,
pub agent_type: String,
pub last_seen: DateTime<Utc>,
}
#[derive(Debug, Clone)]
pub enum GraphOperation {
CreateNode {
node_type: NodeType,
scope: NodeScope,
project_id: Option<String>,
content: String,
confidence: f64,
},
CreateEdge {
source_id: String,
target_id: String,
edge_type: EdgeType,
},
UpdateNode {
id: String,
confidence: Option<f64>,
content: Option<String>,
},
MergeNodes {
keep_id: String,
remove_id: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphAnalysisResponse {
#[serde(default)]
pub reasoning: String,
pub operations: Vec<GraphOperationResponse>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphOperationResponse {
pub action: String,
#[serde(default)]
pub node_type: Option<String>,
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub project_id: Option<String>,
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub confidence: Option<f64>,
#[serde(default)]
pub node_id: Option<String>,
#[serde(default)]
pub new_confidence: Option<f64>,
#[serde(default)]
pub new_content: Option<String>,
#[serde(default)]
pub source_id: Option<String>,
#[serde(default)]
pub target_id: Option<String>,
#[serde(default)]
pub edge_type: Option<String>,
#[serde(default)]
pub keep_id: Option<String>,
#[serde(default)]
pub remove_id: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_projection_status_display() {
assert_eq!(ProjectionStatus::PendingReview.to_string(), "pending_review");
assert_eq!(ProjectionStatus::Applied.to_string(), "applied");
assert_eq!(ProjectionStatus::Dismissed.to_string(), "dismissed");
}
#[test]
fn test_projection_status_from_str() {
assert_eq!(ProjectionStatus::from_str("pending_review"), Some(ProjectionStatus::PendingReview));
assert_eq!(ProjectionStatus::from_str("applied"), Some(ProjectionStatus::Applied));
assert_eq!(ProjectionStatus::from_str("dismissed"), Some(ProjectionStatus::Dismissed));
assert_eq!(ProjectionStatus::from_str("unknown"), None);
}
#[test]
fn test_claude_md_edit_type_serde() {
let edit = ClaudeMdEdit {
edit_type: ClaudeMdEditType::Reword,
original_text: "No async".to_string(),
suggested_content: Some("Sync only — no tokio, no async".to_string()),
target_section: None,
reasoning: "Too terse".to_string(),
};
let json = serde_json::to_string(&edit).unwrap();
let parsed: ClaudeMdEdit = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.edit_type, ClaudeMdEditType::Reword);
assert_eq!(parsed.original_text, "No async");
assert_eq!(parsed.suggested_content.unwrap(), "Sync only — no tokio, no async");
}
#[test]
fn test_claude_md_edit_type_display() {
assert_eq!(ClaudeMdEditType::Add.to_string(), "add");
assert_eq!(ClaudeMdEditType::Remove.to_string(), "remove");
assert_eq!(ClaudeMdEditType::Reword.to_string(), "reword");
assert_eq!(ClaudeMdEditType::Move.to_string(), "move");
}
#[test]
fn test_analysis_response_with_edits() {
let json = r#"{
"reasoning": "test",
"patterns": [],
"claude_md_edits": [
{
"edit_type": "remove",
"original_text": "stale rule",
"reasoning": "no longer relevant"
}
]
}"#;
let resp: AnalysisResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.claude_md_edits.len(), 1);
assert_eq!(resp.claude_md_edits[0].edit_type, ClaudeMdEditType::Remove);
}
#[test]
fn test_analysis_response_without_edits() {
let json = r#"{"reasoning": "test", "patterns": []}"#;
let resp: AnalysisResponse = serde_json::from_str(json).unwrap();
assert!(resp.claude_md_edits.is_empty());
}
#[test]
fn test_node_type_display_and_from_str() {
assert_eq!(NodeType::Preference.to_string(), "preference");
assert_eq!(NodeType::Pattern.to_string(), "pattern");
assert_eq!(NodeType::Rule.to_string(), "rule");
assert_eq!(NodeType::Skill.to_string(), "skill");
assert_eq!(NodeType::Memory.to_string(), "memory");
assert_eq!(NodeType::Directive.to_string(), "directive");
assert_eq!(NodeType::from_str("rule"), NodeType::Rule);
assert_eq!(NodeType::from_str("unknown"), NodeType::Pattern); }
#[test]
fn test_node_scope_display_and_from_str() {
assert_eq!(NodeScope::Global.to_string(), "global");
assert_eq!(NodeScope::Project.to_string(), "project");
assert_eq!(NodeScope::from_str("global"), NodeScope::Global);
assert_eq!(NodeScope::from_str("unknown"), NodeScope::Project); }
#[test]
fn test_node_status_display_and_from_str() {
assert_eq!(NodeStatus::Active.to_string(), "active");
assert_eq!(NodeStatus::PendingReview.to_string(), "pending_review");
assert_eq!(NodeStatus::Dismissed.to_string(), "dismissed");
assert_eq!(NodeStatus::Archived.to_string(), "archived");
assert_eq!(NodeStatus::from_str("pending_review"), NodeStatus::PendingReview);
assert_eq!(NodeStatus::from_str("unknown"), NodeStatus::Active); }
#[test]
fn test_edge_type_display_and_from_str() {
assert_eq!(EdgeType::Supports.to_string(), "supports");
assert_eq!(EdgeType::Contradicts.to_string(), "contradicts");
assert_eq!(EdgeType::Supersedes.to_string(), "supersedes");
assert_eq!(EdgeType::DerivedFrom.to_string(), "derived_from");
assert_eq!(EdgeType::AppliesTo.to_string(), "applies_to");
assert_eq!(EdgeType::from_str("contradicts"), Some(EdgeType::Contradicts));
assert_eq!(EdgeType::from_str("unknown"), None);
}
#[test]
fn test_knowledge_node_struct() {
let node = KnowledgeNode {
id: "test-id".to_string(),
node_type: NodeType::Rule,
scope: NodeScope::Project,
project_id: Some("my-app".to_string()),
content: "Always run tests".to_string(),
confidence: 0.85,
status: NodeStatus::Active,
created_at: Utc::now(),
updated_at: Utc::now(),
projected_at: None,
pr_url: None,
};
assert_eq!(node.node_type, NodeType::Rule);
assert_eq!(node.scope, NodeScope::Project);
assert!(node.project_id.is_some());
}
#[test]
fn test_knowledge_edge_struct() {
let edge = KnowledgeEdge {
source_id: "node-1".to_string(),
target_id: "node-2".to_string(),
edge_type: EdgeType::Supports,
created_at: Utc::now(),
};
assert_eq!(edge.edge_type, EdgeType::Supports);
}
#[test]
fn test_graph_analysis_response_deserialize() {
let json = r#"{
"reasoning": "Found a recurring pattern",
"operations": [
{
"action": "create_node",
"node_type": "rule",
"scope": "project",
"content": "Always run tests",
"confidence": 0.85
},
{
"action": "update_node",
"node_id": "existing-1",
"new_confidence": 0.9
}
]
}"#;
let resp: GraphAnalysisResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.operations.len(), 2);
assert_eq!(resp.reasoning, "Found a recurring pattern");
}
}