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)]
595pub struct AnalyzeV2Result {
596 pub sessions_analyzed: usize,
597 pub nodes_created: usize,
598 pub nodes_updated: usize,
599 pub edges_created: usize,
600 pub nodes_merged: usize,
601 pub input_tokens: u64,
602 pub output_tokens: u64,
603 pub batch_count: usize,
604}
605
606#[derive(Debug, Clone, Serialize)]
608pub struct CompactSession {
609 pub session_id: String,
610 pub project: String,
611 pub user_messages: Vec<CompactUserMessage>,
612 pub tools_used: Vec<String>,
613 pub errors: Vec<String>,
614 pub thinking_highlights: Vec<String>,
615 pub summaries: Vec<String>,
616}
617
618#[derive(Debug, Clone, Serialize)]
619pub struct CompactUserMessage {
620 pub text: String,
621 #[serde(skip_serializing_if = "Option::is_none")]
622 pub timestamp: Option<String>,
623}
624
625#[derive(Debug, Clone, Serialize)]
627pub struct CompactPattern {
628 pub id: String,
629 pub pattern_type: String,
630 pub description: String,
631 pub confidence: f64,
632 pub times_seen: i64,
633 pub suggested_target: String,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct Projection {
641 pub id: String,
642 pub pattern_id: String,
643 pub target_type: String,
644 pub target_path: String,
645 pub content: String,
646 pub applied_at: DateTime<Utc>,
647 pub pr_url: Option<String>,
648 pub status: ProjectionStatus,
649}
650
651#[derive(Debug, Clone)]
653pub struct SkillDraft {
654 pub name: String,
655 pub content: String,
656 pub pattern_id: String,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
661pub struct SkillValidation {
662 pub valid: bool,
663 #[serde(default)]
664 pub feedback: String,
665}
666
667#[derive(Debug, Clone)]
669pub struct AgentDraft {
670 pub name: String,
671 pub content: String,
672 pub pattern_id: String,
673}
674
675#[derive(Debug, Clone)]
677pub struct ApplyAction {
678 pub pattern_id: String,
679 pub pattern_description: String,
680 pub target_type: SuggestedTarget,
681 pub target_path: String,
682 pub content: String,
683 pub track: ApplyTrack,
684}
685
686#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
688#[serde(rename_all = "snake_case")]
689pub enum ProjectionStatus {
690 PendingReview,
691 Applied,
692 Dismissed,
693}
694
695impl std::fmt::Display for ProjectionStatus {
696 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697 match self {
698 Self::PendingReview => write!(f, "pending_review"),
699 Self::Applied => write!(f, "applied"),
700 Self::Dismissed => write!(f, "dismissed"),
701 }
702 }
703}
704
705impl ProjectionStatus {
706 pub fn from_str(s: &str) -> Option<Self> {
707 match s {
708 "pending_review" => Some(Self::PendingReview),
709 "applied" => Some(Self::Applied),
710 "dismissed" => Some(Self::Dismissed),
711 _ => None,
712 }
713 }
714}
715
716#[derive(Debug, Clone, PartialEq)]
718pub enum ApplyTrack {
719 Personal,
721 Shared,
723}
724
725impl std::fmt::Display for ApplyTrack {
726 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
727 match self {
728 Self::Personal => write!(f, "personal"),
729 Self::Shared => write!(f, "shared"),
730 }
731 }
732}
733
734#[derive(Debug, Clone)]
736pub struct ApplyPlan {
737 pub actions: Vec<ApplyAction>,
738}
739
740impl ApplyPlan {
741 pub fn is_empty(&self) -> bool {
742 self.actions.is_empty()
743 }
744
745 pub fn personal_actions(&self) -> Vec<&ApplyAction> {
746 self.actions.iter().filter(|a| a.track == ApplyTrack::Personal).collect()
747 }
748
749 pub fn shared_actions(&self) -> Vec<&ApplyAction> {
750 self.actions.iter().filter(|a| a.track == ApplyTrack::Shared).collect()
751 }
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
757#[serde(rename_all = "snake_case")]
758pub enum NodeType {
759 Preference,
760 Pattern,
761 Rule,
762 Skill,
763 Memory,
764 Directive,
765}
766
767impl std::fmt::Display for NodeType {
768 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
769 match self {
770 Self::Preference => write!(f, "preference"),
771 Self::Pattern => write!(f, "pattern"),
772 Self::Rule => write!(f, "rule"),
773 Self::Skill => write!(f, "skill"),
774 Self::Memory => write!(f, "memory"),
775 Self::Directive => write!(f, "directive"),
776 }
777 }
778}
779
780impl NodeType {
781 pub fn from_str(s: &str) -> Self {
782 match s {
783 "preference" => Self::Preference,
784 "pattern" => Self::Pattern,
785 "rule" => Self::Rule,
786 "skill" => Self::Skill,
787 "memory" => Self::Memory,
788 "directive" => Self::Directive,
789 _ => Self::Pattern,
790 }
791 }
792}
793
794#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
795#[serde(rename_all = "snake_case")]
796pub enum NodeScope {
797 Global,
798 Project,
799}
800
801impl std::fmt::Display for NodeScope {
802 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
803 match self {
804 Self::Global => write!(f, "global"),
805 Self::Project => write!(f, "project"),
806 }
807 }
808}
809
810impl NodeScope {
811 pub fn from_str(s: &str) -> Self {
812 match s {
813 "global" => Self::Global,
814 _ => Self::Project,
815 }
816 }
817}
818
819#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
820#[serde(rename_all = "snake_case")]
821pub enum NodeStatus {
822 Active,
823 PendingReview,
824 Dismissed,
825 Archived,
826}
827
828impl std::fmt::Display for NodeStatus {
829 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
830 match self {
831 Self::Active => write!(f, "active"),
832 Self::PendingReview => write!(f, "pending_review"),
833 Self::Dismissed => write!(f, "dismissed"),
834 Self::Archived => write!(f, "archived"),
835 }
836 }
837}
838
839impl NodeStatus {
840 pub fn from_str(s: &str) -> Self {
841 match s {
842 "active" => Self::Active,
843 "pending_review" => Self::PendingReview,
844 "dismissed" => Self::Dismissed,
845 "archived" => Self::Archived,
846 _ => Self::Active,
847 }
848 }
849}
850
851#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
852#[serde(rename_all = "snake_case")]
853pub enum EdgeType {
854 Supports,
855 Contradicts,
856 Supersedes,
857 DerivedFrom,
858 AppliesTo,
859}
860
861impl std::fmt::Display for EdgeType {
862 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
863 match self {
864 Self::Supports => write!(f, "supports"),
865 Self::Contradicts => write!(f, "contradicts"),
866 Self::Supersedes => write!(f, "supersedes"),
867 Self::DerivedFrom => write!(f, "derived_from"),
868 Self::AppliesTo => write!(f, "applies_to"),
869 }
870 }
871}
872
873impl EdgeType {
874 pub fn from_str(s: &str) -> Option<Self> {
875 match s {
876 "supports" => Some(Self::Supports),
877 "contradicts" => Some(Self::Contradicts),
878 "supersedes" => Some(Self::Supersedes),
879 "derived_from" => Some(Self::DerivedFrom),
880 "applies_to" => Some(Self::AppliesTo),
881 _ => None,
882 }
883 }
884}
885
886#[derive(Debug, Clone, Serialize, Deserialize)]
888pub struct KnowledgeNode {
889 pub id: String,
890 pub node_type: NodeType,
891 pub scope: NodeScope,
892 pub project_id: Option<String>,
893 pub content: String,
894 pub confidence: f64,
895 pub status: NodeStatus,
896 pub created_at: DateTime<Utc>,
897 pub updated_at: DateTime<Utc>,
898 #[serde(default)]
899 pub projected_at: Option<String>,
900 #[serde(default)]
901 pub pr_url: Option<String>,
902}
903
904#[derive(Debug, Clone, Serialize, Deserialize)]
906pub struct KnowledgeEdge {
907 pub source_id: String,
908 pub target_id: String,
909 pub edge_type: EdgeType,
910 pub created_at: DateTime<Utc>,
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize)]
915pub struct KnowledgeProject {
916 pub id: String,
917 pub path: String,
918 pub remote_url: Option<String>,
919 pub agent_type: String,
920 pub last_seen: DateTime<Utc>,
921}
922
923#[derive(Debug, Clone)]
925pub enum GraphOperation {
926 CreateNode {
927 node_type: NodeType,
928 scope: NodeScope,
929 project_id: Option<String>,
930 content: String,
931 confidence: f64,
932 },
933 CreateEdge {
934 source_id: String,
935 target_id: String,
936 edge_type: EdgeType,
937 },
938 UpdateNode {
939 id: String,
940 confidence: Option<f64>,
941 content: Option<String>,
942 },
943 MergeNodes {
944 keep_id: String,
945 remove_id: String,
946 },
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize)]
951pub struct GraphAnalysisResponse {
952 #[serde(default)]
953 pub reasoning: String,
954 pub operations: Vec<GraphOperationResponse>,
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize)]
959pub struct GraphOperationResponse {
960 pub action: String,
961 #[serde(default)]
962 pub node_type: Option<String>,
963 #[serde(default)]
964 pub scope: Option<String>,
965 #[serde(default)]
966 pub project_id: Option<String>,
967 #[serde(default)]
968 pub content: Option<String>,
969 #[serde(default)]
970 pub confidence: Option<f64>,
971 #[serde(default)]
972 pub node_id: Option<String>,
973 #[serde(default)]
974 pub new_confidence: Option<f64>,
975 #[serde(default)]
976 pub new_content: Option<String>,
977 #[serde(default)]
978 pub source_id: Option<String>,
979 #[serde(default)]
980 pub target_id: Option<String>,
981 #[serde(default)]
982 pub edge_type: Option<String>,
983 #[serde(default)]
984 pub keep_id: Option<String>,
985 #[serde(default)]
986 pub remove_id: Option<String>,
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992
993 #[test]
994 fn test_projection_status_display() {
995 assert_eq!(ProjectionStatus::PendingReview.to_string(), "pending_review");
996 assert_eq!(ProjectionStatus::Applied.to_string(), "applied");
997 assert_eq!(ProjectionStatus::Dismissed.to_string(), "dismissed");
998 }
999
1000 #[test]
1001 fn test_projection_status_from_str() {
1002 assert_eq!(ProjectionStatus::from_str("pending_review"), Some(ProjectionStatus::PendingReview));
1003 assert_eq!(ProjectionStatus::from_str("applied"), Some(ProjectionStatus::Applied));
1004 assert_eq!(ProjectionStatus::from_str("dismissed"), Some(ProjectionStatus::Dismissed));
1005 assert_eq!(ProjectionStatus::from_str("unknown"), None);
1006 }
1007
1008 #[test]
1009 fn test_claude_md_edit_type_serde() {
1010 let edit = ClaudeMdEdit {
1011 edit_type: ClaudeMdEditType::Reword,
1012 original_text: "No async".to_string(),
1013 suggested_content: Some("Sync only — no tokio, no async".to_string()),
1014 target_section: None,
1015 reasoning: "Too terse".to_string(),
1016 };
1017 let json = serde_json::to_string(&edit).unwrap();
1018 let parsed: ClaudeMdEdit = serde_json::from_str(&json).unwrap();
1019 assert_eq!(parsed.edit_type, ClaudeMdEditType::Reword);
1020 assert_eq!(parsed.original_text, "No async");
1021 assert_eq!(parsed.suggested_content.unwrap(), "Sync only — no tokio, no async");
1022 }
1023
1024 #[test]
1025 fn test_claude_md_edit_type_display() {
1026 assert_eq!(ClaudeMdEditType::Add.to_string(), "add");
1027 assert_eq!(ClaudeMdEditType::Remove.to_string(), "remove");
1028 assert_eq!(ClaudeMdEditType::Reword.to_string(), "reword");
1029 assert_eq!(ClaudeMdEditType::Move.to_string(), "move");
1030 }
1031
1032 #[test]
1033 fn test_analysis_response_with_edits() {
1034 let json = r#"{
1035 "reasoning": "test",
1036 "patterns": [],
1037 "claude_md_edits": [
1038 {
1039 "edit_type": "remove",
1040 "original_text": "stale rule",
1041 "reasoning": "no longer relevant"
1042 }
1043 ]
1044 }"#;
1045 let resp: AnalysisResponse = serde_json::from_str(json).unwrap();
1046 assert_eq!(resp.claude_md_edits.len(), 1);
1047 assert_eq!(resp.claude_md_edits[0].edit_type, ClaudeMdEditType::Remove);
1048 }
1049
1050 #[test]
1051 fn test_analysis_response_without_edits() {
1052 let json = r#"{"reasoning": "test", "patterns": []}"#;
1053 let resp: AnalysisResponse = serde_json::from_str(json).unwrap();
1054 assert!(resp.claude_md_edits.is_empty());
1055 }
1056
1057 #[test]
1058 fn test_node_type_display_and_from_str() {
1059 assert_eq!(NodeType::Preference.to_string(), "preference");
1060 assert_eq!(NodeType::Pattern.to_string(), "pattern");
1061 assert_eq!(NodeType::Rule.to_string(), "rule");
1062 assert_eq!(NodeType::Skill.to_string(), "skill");
1063 assert_eq!(NodeType::Memory.to_string(), "memory");
1064 assert_eq!(NodeType::Directive.to_string(), "directive");
1065 assert_eq!(NodeType::from_str("rule"), NodeType::Rule);
1066 assert_eq!(NodeType::from_str("unknown"), NodeType::Pattern); }
1068
1069 #[test]
1070 fn test_node_scope_display_and_from_str() {
1071 assert_eq!(NodeScope::Global.to_string(), "global");
1072 assert_eq!(NodeScope::Project.to_string(), "project");
1073 assert_eq!(NodeScope::from_str("global"), NodeScope::Global);
1074 assert_eq!(NodeScope::from_str("unknown"), NodeScope::Project); }
1076
1077 #[test]
1078 fn test_node_status_display_and_from_str() {
1079 assert_eq!(NodeStatus::Active.to_string(), "active");
1080 assert_eq!(NodeStatus::PendingReview.to_string(), "pending_review");
1081 assert_eq!(NodeStatus::Dismissed.to_string(), "dismissed");
1082 assert_eq!(NodeStatus::Archived.to_string(), "archived");
1083 assert_eq!(NodeStatus::from_str("pending_review"), NodeStatus::PendingReview);
1084 assert_eq!(NodeStatus::from_str("unknown"), NodeStatus::Active); }
1086
1087 #[test]
1088 fn test_edge_type_display_and_from_str() {
1089 assert_eq!(EdgeType::Supports.to_string(), "supports");
1090 assert_eq!(EdgeType::Contradicts.to_string(), "contradicts");
1091 assert_eq!(EdgeType::Supersedes.to_string(), "supersedes");
1092 assert_eq!(EdgeType::DerivedFrom.to_string(), "derived_from");
1093 assert_eq!(EdgeType::AppliesTo.to_string(), "applies_to");
1094 assert_eq!(EdgeType::from_str("contradicts"), Some(EdgeType::Contradicts));
1095 assert_eq!(EdgeType::from_str("unknown"), None);
1096 }
1097
1098 #[test]
1099 fn test_knowledge_node_struct() {
1100 let node = KnowledgeNode {
1101 id: "test-id".to_string(),
1102 node_type: NodeType::Rule,
1103 scope: NodeScope::Project,
1104 project_id: Some("my-app".to_string()),
1105 content: "Always run tests".to_string(),
1106 confidence: 0.85,
1107 status: NodeStatus::Active,
1108 created_at: Utc::now(),
1109 updated_at: Utc::now(),
1110 projected_at: None,
1111 pr_url: None,
1112 };
1113 assert_eq!(node.node_type, NodeType::Rule);
1114 assert_eq!(node.scope, NodeScope::Project);
1115 assert!(node.project_id.is_some());
1116 }
1117
1118 #[test]
1119 fn test_knowledge_edge_struct() {
1120 let edge = KnowledgeEdge {
1121 source_id: "node-1".to_string(),
1122 target_id: "node-2".to_string(),
1123 edge_type: EdgeType::Supports,
1124 created_at: Utc::now(),
1125 };
1126 assert_eq!(edge.edge_type, EdgeType::Supports);
1127 }
1128
1129 #[test]
1130 fn test_graph_analysis_response_deserialize() {
1131 let json = r#"{
1132 "reasoning": "Found a recurring pattern",
1133 "operations": [
1134 {
1135 "action": "create_node",
1136 "node_type": "rule",
1137 "scope": "project",
1138 "content": "Always run tests",
1139 "confidence": 0.85
1140 },
1141 {
1142 "action": "update_node",
1143 "node_id": "existing-1",
1144 "new_confidence": 0.9
1145 }
1146 ]
1147 }"#;
1148 let resp: GraphAnalysisResponse = serde_json::from_str(json).unwrap();
1149 assert_eq!(resp.operations.len(), 2);
1150 assert_eq!(resp.reasoning, "Found a recurring pattern");
1151 }
1152}