1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Deserializer, Serialize};
3
4fn null_to_empty<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
6 Option::<String>::deserialize(d).map(|o| o.unwrap_or_default())
7}
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[serde(rename_all = "snake_case")]
13pub enum PatternType {
14 RepetitiveInstruction,
15 RecurringMistake,
16 WorkflowPattern,
17 StaleContext,
18 RedundantContext,
19}
20
21impl std::fmt::Display for PatternType {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 Self::RepetitiveInstruction => write!(f, "repetitive_instruction"),
25 Self::RecurringMistake => write!(f, "recurring_mistake"),
26 Self::WorkflowPattern => write!(f, "workflow_pattern"),
27 Self::StaleContext => write!(f, "stale_context"),
28 Self::RedundantContext => write!(f, "redundant_context"),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34#[serde(rename_all = "snake_case")]
35pub enum PatternStatus {
36 Discovered,
37 Active,
38 Archived,
39 Dismissed,
40}
41
42impl std::fmt::Display for PatternStatus {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::Discovered => write!(f, "discovered"),
46 Self::Active => write!(f, "active"),
47 Self::Archived => write!(f, "archived"),
48 Self::Dismissed => write!(f, "dismissed"),
49 }
50 }
51}
52
53impl PatternStatus {
54 pub fn from_str(s: &str) -> Self {
55 match s {
56 "discovered" => Self::Discovered,
57 "active" => Self::Active,
58 "archived" => Self::Archived,
59 "dismissed" => Self::Dismissed,
60 _ => Self::Discovered,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
66#[serde(rename_all = "snake_case")]
67pub enum SuggestedTarget {
68 Skill,
69 ClaudeMd,
70 GlobalAgent,
71 DbOnly,
72}
73
74impl std::fmt::Display for SuggestedTarget {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::Skill => write!(f, "skill"),
78 Self::ClaudeMd => write!(f, "claude_md"),
79 Self::GlobalAgent => write!(f, "global_agent"),
80 Self::DbOnly => write!(f, "db_only"),
81 }
82 }
83}
84
85impl SuggestedTarget {
86 pub fn from_str(s: &str) -> Self {
87 match s {
88 "skill" => Self::Skill,
89 "claude_md" => Self::ClaudeMd,
90 "global_agent" => Self::GlobalAgent,
91 "db_only" => Self::DbOnly,
92 _ => Self::DbOnly,
93 }
94 }
95}
96
97impl PatternType {
98 pub fn from_str(s: &str) -> Self {
99 match s {
100 "repetitive_instruction" => Self::RepetitiveInstruction,
101 "recurring_mistake" => Self::RecurringMistake,
102 "workflow_pattern" => Self::WorkflowPattern,
103 "stale_context" => Self::StaleContext,
104 "redundant_context" => Self::RedundantContext,
105 _ => Self::WorkflowPattern,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct Pattern {
112 pub id: String,
113 pub pattern_type: PatternType,
114 pub description: String,
115 pub confidence: f64,
116 pub times_seen: i64,
117 pub first_seen: DateTime<Utc>,
118 pub last_seen: DateTime<Utc>,
119 pub last_projected: Option<DateTime<Utc>>,
120 pub status: PatternStatus,
121 pub source_sessions: Vec<String>,
122 pub related_files: Vec<String>,
123 pub suggested_content: String,
124 pub suggested_target: SuggestedTarget,
125 pub project: Option<String>,
126 pub generation_failed: bool,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(tag = "type")]
134pub enum SessionEntry {
135 #[serde(rename = "user")]
136 User(UserEntry),
137 #[serde(rename = "assistant")]
138 Assistant(AssistantEntry),
139 #[serde(rename = "summary")]
140 Summary(SummaryEntry),
141 #[serde(rename = "file-history-snapshot")]
142 FileHistorySnapshot(serde_json::Value),
143 #[serde(rename = "progress")]
144 Progress(serde_json::Value),
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct UserEntry {
150 pub uuid: String,
151 #[serde(default)]
152 pub parent_uuid: Option<String>,
153 #[serde(default)]
154 pub session_id: Option<String>,
155 #[serde(default)]
156 pub cwd: Option<String>,
157 #[serde(default)]
158 pub version: Option<String>,
159 #[serde(default)]
160 pub git_branch: Option<String>,
161 #[serde(default)]
162 pub timestamp: Option<String>,
163 pub message: UserMessage,
164 #[serde(default)]
165 pub is_sidechain: bool,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct UserMessage {
170 pub role: String,
171 pub content: MessageContent,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(untagged)]
176pub enum MessageContent {
177 Text(String),
178 Blocks(Vec<ContentBlock>),
179 Other(serde_json::Value),
181}
182
183impl MessageContent {
184 pub fn as_text(&self) -> String {
186 match self {
187 MessageContent::Text(s) => s.clone(),
188 MessageContent::Blocks(blocks) => {
189 let mut parts = Vec::new();
190 for block in blocks {
191 match block {
192 ContentBlock::Text { text } => parts.push(text.clone()),
193 ContentBlock::ToolResult { content, .. } => {
194 if let Some(c) = content {
195 parts.push(c.as_text());
196 }
197 }
198 _ => {}
199 }
200 }
201 parts.join("\n")
202 }
203 MessageContent::Other(_) => String::new(),
204 }
205 }
206
207 pub fn is_tool_result(&self) -> bool {
209 matches!(self, MessageContent::Blocks(blocks) if blocks.iter().any(|b| matches!(b, ContentBlock::ToolResult { .. })))
210 }
211
212 pub fn is_unknown(&self) -> bool {
214 matches!(self, MessageContent::Other(_))
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219#[serde(tag = "type")]
220pub enum ContentBlock {
221 #[serde(rename = "text")]
222 Text { text: String },
223 #[serde(rename = "thinking")]
224 Thinking {
225 thinking: String,
226 #[serde(default)]
227 signature: Option<String>,
228 },
229 #[serde(rename = "tool_use")]
230 ToolUse {
231 id: String,
232 name: String,
233 #[serde(default)]
234 input: serde_json::Value,
235 },
236 #[serde(rename = "tool_result")]
237 ToolResult {
238 tool_use_id: String,
239 #[serde(default)]
240 content: Option<ToolResultContent>,
241 },
242 #[serde(other)]
244 Unknown,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(untagged)]
250pub enum ToolResultContent {
251 Text(String),
252 Blocks(Vec<serde_json::Value>),
253}
254
255impl ToolResultContent {
256 pub fn as_text(&self) -> String {
257 match self {
258 Self::Text(s) => s.clone(),
259 Self::Blocks(blocks) => {
260 blocks
261 .iter()
262 .filter_map(|b| b.get("text").and_then(|t| t.as_str()))
263 .collect::<Vec<_>>()
264 .join("\n")
265 }
266 }
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub struct AssistantEntry {
273 pub uuid: String,
274 #[serde(default)]
275 pub parent_uuid: Option<String>,
276 #[serde(default)]
277 pub session_id: Option<String>,
278 #[serde(default)]
279 pub cwd: Option<String>,
280 #[serde(default)]
281 pub version: Option<String>,
282 #[serde(default)]
283 pub git_branch: Option<String>,
284 #[serde(default)]
285 pub timestamp: Option<String>,
286 pub message: AssistantMessage,
287 #[serde(default)]
288 pub is_sidechain: bool,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct AssistantMessage {
293 pub role: String,
294 #[serde(default)]
295 pub model: Option<String>,
296 #[serde(default)]
297 pub content: Vec<ContentBlock>,
298 #[serde(default)]
299 pub stop_reason: Option<String>,
300 #[serde(default)]
301 pub usage: Option<Usage>,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct Usage {
306 #[serde(default)]
307 pub input_tokens: u64,
308 #[serde(default)]
309 pub output_tokens: u64,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct SummaryEntry {
315 #[serde(default)]
316 pub uuid: String,
317 #[serde(default)]
318 pub parent_uuid: Option<String>,
319 #[serde(default)]
320 pub session_id: Option<String>,
321 #[serde(default)]
322 pub timestamp: Option<String>,
323 #[serde(default)]
324 pub summary: Option<String>,
325 #[serde(default)]
327 pub message: Option<serde_json::Value>,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct Session {
335 pub session_id: String,
336 pub project: String,
337 pub session_path: String,
338 pub user_messages: Vec<ParsedUserMessage>,
339 pub assistant_messages: Vec<ParsedAssistantMessage>,
340 pub summaries: Vec<String>,
341 pub tools_used: Vec<String>,
342 pub errors: Vec<String>,
343 pub metadata: SessionMetadata,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ParsedUserMessage {
348 pub text: String,
349 pub timestamp: Option<String>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ParsedAssistantMessage {
354 pub text: String,
355 pub thinking_summary: Option<String>,
356 pub tools: Vec<String>,
357 pub timestamp: Option<String>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct SessionMetadata {
362 pub cwd: Option<String>,
363 pub version: Option<String>,
364 pub git_branch: Option<String>,
365 pub model: Option<String>,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct HistoryEntry {
373 #[serde(default)]
374 pub display: Option<String>,
375 #[serde(default)]
376 pub timestamp: Option<u64>,
377 #[serde(default)]
378 pub project: Option<String>,
379 #[serde(default)]
380 pub session_id: Option<String>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct PluginSkillSummary {
387 pub plugin_name: String,
388 pub skill_name: String,
389 pub description: String,
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct ContextSnapshot {
394 pub claude_md: Option<String>,
395 pub skills: Vec<SkillFile>,
396 pub memory_md: Option<String>,
397 pub global_agents: Vec<AgentFile>,
398 #[serde(default)]
399 pub plugin_skills: Vec<PluginSkillSummary>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct SkillFile {
404 pub path: String,
405 pub content: String,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct AgentFile {
410 pub path: String,
411 pub content: String,
412}
413
414#[derive(Debug, Clone)]
417pub struct IngestedSession {
418 pub session_id: String,
419 pub project: String,
420 pub session_path: String,
421 pub file_size: u64,
422 pub file_mtime: String,
423 pub ingested_at: DateTime<Utc>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
430#[serde(tag = "action")]
431pub enum PatternUpdate {
432 #[serde(rename = "new")]
433 New(NewPattern),
434 #[serde(rename = "update")]
435 Update(UpdateExisting),
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
439pub struct NewPattern {
440 pub pattern_type: PatternType,
441 #[serde(deserialize_with = "null_to_empty")]
442 pub description: String,
443 pub confidence: f64,
444 #[serde(default)]
445 pub source_sessions: Vec<String>,
446 #[serde(default)]
447 pub related_files: Vec<String>,
448 #[serde(default, deserialize_with = "null_to_empty")]
449 pub suggested_content: String,
450 pub suggested_target: SuggestedTarget,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct UpdateExisting {
455 #[serde(deserialize_with = "null_to_empty")]
456 pub existing_id: String,
457 #[serde(default)]
458 pub new_sessions: Vec<String>,
459 pub new_confidence: f64,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize)]
464pub struct AnalysisResponse {
465 #[serde(default)]
466 pub reasoning: String,
467 pub patterns: Vec<PatternUpdate>,
468 #[serde(default)]
469 pub claude_md_edits: Vec<ClaudeMdEdit>,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
475#[serde(rename_all = "snake_case")]
476pub enum ClaudeMdEditType {
477 Add,
478 Remove,
479 Reword,
480 Move,
481}
482
483impl std::fmt::Display for ClaudeMdEditType {
484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485 match self {
486 Self::Add => write!(f, "add"),
487 Self::Remove => write!(f, "remove"),
488 Self::Reword => write!(f, "reword"),
489 Self::Move => write!(f, "move"),
490 }
491 }
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct ClaudeMdEdit {
497 pub edit_type: ClaudeMdEditType,
498 #[serde(default)]
499 pub original_text: String,
500 #[serde(default)]
501 pub suggested_content: Option<String>,
502 #[serde(default)]
503 pub target_section: Option<String>,
504 #[serde(default)]
505 pub reasoning: String,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct ClaudeCliOutput {
511 #[serde(default)]
512 pub result: Option<String>,
513 #[serde(default)]
514 pub is_error: bool,
515 #[serde(default)]
516 pub duration_ms: u64,
517 #[serde(default)]
518 pub num_turns: u64,
519 #[serde(default)]
520 pub stop_reason: Option<String>,
521 #[serde(default)]
522 pub session_id: Option<String>,
523 #[serde(default)]
524 pub usage: Option<CliUsage>,
525 #[serde(default)]
528 pub structured_output: Option<serde_json::Value>,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct CliUsage {
534 #[serde(default)]
535 pub input_tokens: u64,
536 #[serde(default)]
537 pub output_tokens: u64,
538 #[serde(default)]
539 pub cache_creation_input_tokens: u64,
540 #[serde(default)]
541 pub cache_read_input_tokens: u64,
542}
543
544impl ClaudeCliOutput {
545 pub fn total_input_tokens(&self) -> u64 {
547 self.usage.as_ref().map_or(0, |u| {
548 u.input_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens
549 })
550 }
551
552 pub fn total_output_tokens(&self) -> u64 {
554 self.usage.as_ref().map_or(0, |u| u.output_tokens)
555 }
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct AuditEntry {
561 pub timestamp: DateTime<Utc>,
562 pub action: String,
563 pub details: serde_json::Value,
564}
565
566#[derive(Debug, Clone)]
568pub struct BatchDetail {
569 pub batch_index: usize,
570 pub session_count: usize,
571 pub session_ids: Vec<String>,
572 pub prompt_chars: usize,
573 pub input_tokens: u64,
574 pub output_tokens: u64,
575 pub new_patterns: usize,
576 pub updated_patterns: usize,
577 pub reasoning: String,
578 pub ai_response_preview: String,
579}
580
581#[derive(Debug, Clone)]
583pub struct AnalyzeResult {
584 pub sessions_analyzed: usize,
585 pub new_patterns: usize,
586 pub updated_patterns: usize,
587 pub total_patterns: usize,
588 pub input_tokens: u64,
589 pub output_tokens: u64,
590 pub batch_details: Vec<BatchDetail>,
591}
592
593#[derive(Debug, Clone, Serialize)]
595pub struct CompactSession {
596 pub session_id: String,
597 pub project: String,
598 pub user_messages: Vec<CompactUserMessage>,
599 pub tools_used: Vec<String>,
600 pub errors: Vec<String>,
601 pub thinking_highlights: Vec<String>,
602 pub summaries: Vec<String>,
603}
604
605#[derive(Debug, Clone, Serialize)]
606pub struct CompactUserMessage {
607 pub text: String,
608 #[serde(skip_serializing_if = "Option::is_none")]
609 pub timestamp: Option<String>,
610}
611
612#[derive(Debug, Clone, Serialize)]
614pub struct CompactPattern {
615 pub id: String,
616 pub pattern_type: String,
617 pub description: String,
618 pub confidence: f64,
619 pub times_seen: i64,
620 pub suggested_target: String,
621}
622
623#[derive(Debug, Clone, Serialize, Deserialize)]
627pub struct Projection {
628 pub id: String,
629 pub pattern_id: String,
630 pub target_type: String,
631 pub target_path: String,
632 pub content: String,
633 pub applied_at: DateTime<Utc>,
634 pub pr_url: Option<String>,
635 pub status: ProjectionStatus,
636}
637
638#[derive(Debug, Clone)]
640pub struct SkillDraft {
641 pub name: String,
642 pub content: String,
643 pub pattern_id: String,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct SkillValidation {
649 pub valid: bool,
650 #[serde(default)]
651 pub feedback: String,
652}
653
654#[derive(Debug, Clone)]
656pub struct AgentDraft {
657 pub name: String,
658 pub content: String,
659 pub pattern_id: String,
660}
661
662#[derive(Debug, Clone)]
664pub struct ApplyAction {
665 pub pattern_id: String,
666 pub pattern_description: String,
667 pub target_type: SuggestedTarget,
668 pub target_path: String,
669 pub content: String,
670 pub track: ApplyTrack,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
675#[serde(rename_all = "snake_case")]
676pub enum ProjectionStatus {
677 PendingReview,
678 Applied,
679 Dismissed,
680}
681
682impl std::fmt::Display for ProjectionStatus {
683 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684 match self {
685 Self::PendingReview => write!(f, "pending_review"),
686 Self::Applied => write!(f, "applied"),
687 Self::Dismissed => write!(f, "dismissed"),
688 }
689 }
690}
691
692impl ProjectionStatus {
693 pub fn from_str(s: &str) -> Option<Self> {
694 match s {
695 "pending_review" => Some(Self::PendingReview),
696 "applied" => Some(Self::Applied),
697 "dismissed" => Some(Self::Dismissed),
698 _ => None,
699 }
700 }
701}
702
703#[derive(Debug, Clone, PartialEq)]
705pub enum ApplyTrack {
706 Personal,
708 Shared,
710}
711
712impl std::fmt::Display for ApplyTrack {
713 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
714 match self {
715 Self::Personal => write!(f, "personal"),
716 Self::Shared => write!(f, "shared"),
717 }
718 }
719}
720
721#[derive(Debug, Clone)]
723pub struct ApplyPlan {
724 pub actions: Vec<ApplyAction>,
725}
726
727impl ApplyPlan {
728 pub fn is_empty(&self) -> bool {
729 self.actions.is_empty()
730 }
731
732 pub fn personal_actions(&self) -> Vec<&ApplyAction> {
733 self.actions.iter().filter(|a| a.track == ApplyTrack::Personal).collect()
734 }
735
736 pub fn shared_actions(&self) -> Vec<&ApplyAction> {
737 self.actions.iter().filter(|a| a.track == ApplyTrack::Shared).collect()
738 }
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
744#[serde(rename_all = "snake_case")]
745pub enum NodeType {
746 Preference,
747 Pattern,
748 Rule,
749 Skill,
750 Memory,
751 Directive,
752}
753
754impl std::fmt::Display for NodeType {
755 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
756 match self {
757 Self::Preference => write!(f, "preference"),
758 Self::Pattern => write!(f, "pattern"),
759 Self::Rule => write!(f, "rule"),
760 Self::Skill => write!(f, "skill"),
761 Self::Memory => write!(f, "memory"),
762 Self::Directive => write!(f, "directive"),
763 }
764 }
765}
766
767impl NodeType {
768 pub fn from_str(s: &str) -> Self {
769 match s {
770 "preference" => Self::Preference,
771 "pattern" => Self::Pattern,
772 "rule" => Self::Rule,
773 "skill" => Self::Skill,
774 "memory" => Self::Memory,
775 "directive" => Self::Directive,
776 _ => Self::Pattern,
777 }
778 }
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
782#[serde(rename_all = "snake_case")]
783pub enum NodeScope {
784 Global,
785 Project,
786}
787
788impl std::fmt::Display for NodeScope {
789 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
790 match self {
791 Self::Global => write!(f, "global"),
792 Self::Project => write!(f, "project"),
793 }
794 }
795}
796
797impl NodeScope {
798 pub fn from_str(s: &str) -> Self {
799 match s {
800 "global" => Self::Global,
801 _ => Self::Project,
802 }
803 }
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
807#[serde(rename_all = "snake_case")]
808pub enum NodeStatus {
809 Active,
810 PendingReview,
811 Dismissed,
812 Archived,
813}
814
815impl std::fmt::Display for NodeStatus {
816 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
817 match self {
818 Self::Active => write!(f, "active"),
819 Self::PendingReview => write!(f, "pending_review"),
820 Self::Dismissed => write!(f, "dismissed"),
821 Self::Archived => write!(f, "archived"),
822 }
823 }
824}
825
826impl NodeStatus {
827 pub fn from_str(s: &str) -> Self {
828 match s {
829 "active" => Self::Active,
830 "pending_review" => Self::PendingReview,
831 "dismissed" => Self::Dismissed,
832 "archived" => Self::Archived,
833 _ => Self::Active,
834 }
835 }
836}
837
838#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
839#[serde(rename_all = "snake_case")]
840pub enum EdgeType {
841 Supports,
842 Contradicts,
843 Supersedes,
844 DerivedFrom,
845 AppliesTo,
846}
847
848impl std::fmt::Display for EdgeType {
849 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
850 match self {
851 Self::Supports => write!(f, "supports"),
852 Self::Contradicts => write!(f, "contradicts"),
853 Self::Supersedes => write!(f, "supersedes"),
854 Self::DerivedFrom => write!(f, "derived_from"),
855 Self::AppliesTo => write!(f, "applies_to"),
856 }
857 }
858}
859
860impl EdgeType {
861 pub fn from_str(s: &str) -> Option<Self> {
862 match s {
863 "supports" => Some(Self::Supports),
864 "contradicts" => Some(Self::Contradicts),
865 "supersedes" => Some(Self::Supersedes),
866 "derived_from" => Some(Self::DerivedFrom),
867 "applies_to" => Some(Self::AppliesTo),
868 _ => None,
869 }
870 }
871}
872
873#[derive(Debug, Clone, Serialize, Deserialize)]
875pub struct KnowledgeNode {
876 pub id: String,
877 pub node_type: NodeType,
878 pub scope: NodeScope,
879 pub project_id: Option<String>,
880 pub content: String,
881 pub confidence: f64,
882 pub status: NodeStatus,
883 pub created_at: DateTime<Utc>,
884 pub updated_at: DateTime<Utc>,
885 #[serde(default)]
886 pub projected_at: Option<String>,
887 #[serde(default)]
888 pub pr_url: Option<String>,
889}
890
891#[derive(Debug, Clone, Serialize, Deserialize)]
893pub struct KnowledgeEdge {
894 pub source_id: String,
895 pub target_id: String,
896 pub edge_type: EdgeType,
897 pub created_at: DateTime<Utc>,
898}
899
900#[derive(Debug, Clone, Serialize, Deserialize)]
902pub struct KnowledgeProject {
903 pub id: String,
904 pub path: String,
905 pub remote_url: Option<String>,
906 pub agent_type: String,
907 pub last_seen: DateTime<Utc>,
908}
909
910#[derive(Debug, Clone)]
912pub enum GraphOperation {
913 CreateNode {
914 node_type: NodeType,
915 scope: NodeScope,
916 project_id: Option<String>,
917 content: String,
918 confidence: f64,
919 },
920 CreateEdge {
921 source_id: String,
922 target_id: String,
923 edge_type: EdgeType,
924 },
925 UpdateNode {
926 id: String,
927 confidence: Option<f64>,
928 content: Option<String>,
929 },
930 MergeNodes {
931 keep_id: String,
932 remove_id: String,
933 },
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize)]
938pub struct GraphAnalysisResponse {
939 #[serde(default)]
940 pub reasoning: String,
941 pub operations: Vec<GraphOperationResponse>,
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize)]
946pub struct GraphOperationResponse {
947 pub action: String,
948 #[serde(default)]
949 pub node_type: Option<String>,
950 #[serde(default)]
951 pub scope: Option<String>,
952 #[serde(default)]
953 pub project_id: Option<String>,
954 #[serde(default)]
955 pub content: Option<String>,
956 #[serde(default)]
957 pub confidence: Option<f64>,
958 #[serde(default)]
959 pub node_id: Option<String>,
960 #[serde(default)]
961 pub new_confidence: Option<f64>,
962 #[serde(default)]
963 pub new_content: Option<String>,
964 #[serde(default)]
965 pub source_id: Option<String>,
966 #[serde(default)]
967 pub target_id: Option<String>,
968 #[serde(default)]
969 pub edge_type: Option<String>,
970 #[serde(default)]
971 pub keep_id: Option<String>,
972 #[serde(default)]
973 pub remove_id: Option<String>,
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979
980 #[test]
981 fn test_projection_status_display() {
982 assert_eq!(ProjectionStatus::PendingReview.to_string(), "pending_review");
983 assert_eq!(ProjectionStatus::Applied.to_string(), "applied");
984 assert_eq!(ProjectionStatus::Dismissed.to_string(), "dismissed");
985 }
986
987 #[test]
988 fn test_projection_status_from_str() {
989 assert_eq!(ProjectionStatus::from_str("pending_review"), Some(ProjectionStatus::PendingReview));
990 assert_eq!(ProjectionStatus::from_str("applied"), Some(ProjectionStatus::Applied));
991 assert_eq!(ProjectionStatus::from_str("dismissed"), Some(ProjectionStatus::Dismissed));
992 assert_eq!(ProjectionStatus::from_str("unknown"), None);
993 }
994
995 #[test]
996 fn test_claude_md_edit_type_serde() {
997 let edit = ClaudeMdEdit {
998 edit_type: ClaudeMdEditType::Reword,
999 original_text: "No async".to_string(),
1000 suggested_content: Some("Sync only — no tokio, no async".to_string()),
1001 target_section: None,
1002 reasoning: "Too terse".to_string(),
1003 };
1004 let json = serde_json::to_string(&edit).unwrap();
1005 let parsed: ClaudeMdEdit = serde_json::from_str(&json).unwrap();
1006 assert_eq!(parsed.edit_type, ClaudeMdEditType::Reword);
1007 assert_eq!(parsed.original_text, "No async");
1008 assert_eq!(parsed.suggested_content.unwrap(), "Sync only — no tokio, no async");
1009 }
1010
1011 #[test]
1012 fn test_claude_md_edit_type_display() {
1013 assert_eq!(ClaudeMdEditType::Add.to_string(), "add");
1014 assert_eq!(ClaudeMdEditType::Remove.to_string(), "remove");
1015 assert_eq!(ClaudeMdEditType::Reword.to_string(), "reword");
1016 assert_eq!(ClaudeMdEditType::Move.to_string(), "move");
1017 }
1018
1019 #[test]
1020 fn test_analysis_response_with_edits() {
1021 let json = r#"{
1022 "reasoning": "test",
1023 "patterns": [],
1024 "claude_md_edits": [
1025 {
1026 "edit_type": "remove",
1027 "original_text": "stale rule",
1028 "reasoning": "no longer relevant"
1029 }
1030 ]
1031 }"#;
1032 let resp: AnalysisResponse = serde_json::from_str(json).unwrap();
1033 assert_eq!(resp.claude_md_edits.len(), 1);
1034 assert_eq!(resp.claude_md_edits[0].edit_type, ClaudeMdEditType::Remove);
1035 }
1036
1037 #[test]
1038 fn test_analysis_response_without_edits() {
1039 let json = r#"{"reasoning": "test", "patterns": []}"#;
1040 let resp: AnalysisResponse = serde_json::from_str(json).unwrap();
1041 assert!(resp.claude_md_edits.is_empty());
1042 }
1043
1044 #[test]
1045 fn test_node_type_display_and_from_str() {
1046 assert_eq!(NodeType::Preference.to_string(), "preference");
1047 assert_eq!(NodeType::Pattern.to_string(), "pattern");
1048 assert_eq!(NodeType::Rule.to_string(), "rule");
1049 assert_eq!(NodeType::Skill.to_string(), "skill");
1050 assert_eq!(NodeType::Memory.to_string(), "memory");
1051 assert_eq!(NodeType::Directive.to_string(), "directive");
1052 assert_eq!(NodeType::from_str("rule"), NodeType::Rule);
1053 assert_eq!(NodeType::from_str("unknown"), NodeType::Pattern); }
1055
1056 #[test]
1057 fn test_node_scope_display_and_from_str() {
1058 assert_eq!(NodeScope::Global.to_string(), "global");
1059 assert_eq!(NodeScope::Project.to_string(), "project");
1060 assert_eq!(NodeScope::from_str("global"), NodeScope::Global);
1061 assert_eq!(NodeScope::from_str("unknown"), NodeScope::Project); }
1063
1064 #[test]
1065 fn test_node_status_display_and_from_str() {
1066 assert_eq!(NodeStatus::Active.to_string(), "active");
1067 assert_eq!(NodeStatus::PendingReview.to_string(), "pending_review");
1068 assert_eq!(NodeStatus::Dismissed.to_string(), "dismissed");
1069 assert_eq!(NodeStatus::Archived.to_string(), "archived");
1070 assert_eq!(NodeStatus::from_str("pending_review"), NodeStatus::PendingReview);
1071 assert_eq!(NodeStatus::from_str("unknown"), NodeStatus::Active); }
1073
1074 #[test]
1075 fn test_edge_type_display_and_from_str() {
1076 assert_eq!(EdgeType::Supports.to_string(), "supports");
1077 assert_eq!(EdgeType::Contradicts.to_string(), "contradicts");
1078 assert_eq!(EdgeType::Supersedes.to_string(), "supersedes");
1079 assert_eq!(EdgeType::DerivedFrom.to_string(), "derived_from");
1080 assert_eq!(EdgeType::AppliesTo.to_string(), "applies_to");
1081 assert_eq!(EdgeType::from_str("contradicts"), Some(EdgeType::Contradicts));
1082 assert_eq!(EdgeType::from_str("unknown"), None);
1083 }
1084
1085 #[test]
1086 fn test_knowledge_node_struct() {
1087 let node = KnowledgeNode {
1088 id: "test-id".to_string(),
1089 node_type: NodeType::Rule,
1090 scope: NodeScope::Project,
1091 project_id: Some("my-app".to_string()),
1092 content: "Always run tests".to_string(),
1093 confidence: 0.85,
1094 status: NodeStatus::Active,
1095 created_at: Utc::now(),
1096 updated_at: Utc::now(),
1097 projected_at: None,
1098 pr_url: None,
1099 };
1100 assert_eq!(node.node_type, NodeType::Rule);
1101 assert_eq!(node.scope, NodeScope::Project);
1102 assert!(node.project_id.is_some());
1103 }
1104
1105 #[test]
1106 fn test_knowledge_edge_struct() {
1107 let edge = KnowledgeEdge {
1108 source_id: "node-1".to_string(),
1109 target_id: "node-2".to_string(),
1110 edge_type: EdgeType::Supports,
1111 created_at: Utc::now(),
1112 };
1113 assert_eq!(edge.edge_type, EdgeType::Supports);
1114 }
1115
1116 #[test]
1117 fn test_graph_analysis_response_deserialize() {
1118 let json = r#"{
1119 "reasoning": "Found a recurring pattern",
1120 "operations": [
1121 {
1122 "action": "create_node",
1123 "node_type": "rule",
1124 "scope": "project",
1125 "content": "Always run tests",
1126 "confidence": 0.85
1127 },
1128 {
1129 "action": "update_node",
1130 "node_id": "existing-1",
1131 "new_confidence": 0.9
1132 }
1133 ]
1134 }"#;
1135 let resp: GraphAnalysisResponse = serde_json::from_str(json).unwrap();
1136 assert_eq!(resp.operations.len(), 2);
1137 assert_eq!(resp.reasoning, "Found a recurring pattern");
1138 }
1139}