miyabi_types/
issue.rs

1//! Issue and trace logging types
2
3use crate::agent::{AgentResult, AgentStatus, AgentType, EscalationInfo, ImpactLevel};
4use crate::quality::QualityReport;
5use serde::{Deserialize, Serialize};
6
7/// GitHub Issue
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Issue {
10    pub number: u64,
11    pub title: String,
12    pub body: String,
13    pub state: IssueStateGithub,
14    pub labels: Vec<String>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub assignee: Option<String>,
17    pub created_at: chrono::DateTime<chrono::Utc>,
18    pub updated_at: chrono::DateTime<chrono::Utc>,
19    pub url: String,
20}
21
22/// GitHub issue state (open/closed)
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum IssueStateGithub {
26    Open,
27    Closed,
28}
29
30/// Issue state in Miyabi lifecycle (8 states)
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum IssueState {
34    Pending,      // Issue created, awaiting triage
35    Analyzing,    // CoordinatorAgent analyzing
36    Implementing, // Specialist Agents working
37    Reviewing,    // ReviewAgent checking quality
38    Deploying,    // DeploymentAgent deploying
39    Done,         // Completed successfully
40    Blocked,      // Blocked - requires intervention
41    Failed,       // Execution failed
42}
43
44impl IssueState {
45    /// Get corresponding GitHub label for this state
46    pub fn to_label(&self) -> &'static str {
47        match self {
48            IssueState::Pending => "📥 state:pending",
49            IssueState::Analyzing => "🔍 state:analyzing",
50            IssueState::Implementing => "🏗️ state:implementing",
51            IssueState::Reviewing => "👀 state:reviewing",
52            IssueState::Deploying => "🚀 state:deploying",
53            IssueState::Done => "✅ state:done",
54            IssueState::Blocked => "🚫 state:blocked",
55            IssueState::Failed => "❌ state:failed",
56        }
57    }
58}
59
60/// State transition record
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct StateTransition {
63    pub from: IssueState,
64    pub to: IssueState,
65    pub timestamp: chrono::DateTime<chrono::Utc>,
66    pub triggered_by: String, // Agent or user
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub reason: Option<String>,
69}
70
71/// Agent execution record
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct AgentExecution {
74    pub agent_type: AgentType,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub task_id: Option<String>,
77    pub start_time: chrono::DateTime<chrono::Utc>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub end_time: Option<chrono::DateTime<chrono::Utc>>,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub duration_ms: Option<u64>,
82    pub status: AgentStatus,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub result: Option<AgentResult>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub error: Option<String>,
87}
88
89/// Label change record
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct LabelChange {
92    pub timestamp: chrono::DateTime<chrono::Utc>,
93    pub action: LabelAction,
94    pub label: String,
95    pub performed_by: String, // Agent or user
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum LabelAction {
101    Added,
102    Removed,
103}
104
105/// Trace note (manual annotation)
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct TraceNote {
108    pub timestamp: chrono::DateTime<chrono::Utc>,
109    pub author: String, // Agent or user
110    pub content: String,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub tags: Option<Vec<String>>,
113}
114
115/// Pull Request result
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PRResult {
118    pub number: u64,
119    pub url: String,
120    pub state: PRState,
121    pub created_at: chrono::DateTime<chrono::Utc>,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "lowercase")]
126pub enum PRState {
127    Draft,
128    Open,
129    Merged,
130    Closed,
131}
132
133/// Deployment result
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct DeploymentResult {
136    pub environment: Environment,
137    pub version: String,
138    pub project_id: String,
139    pub deployment_url: String,
140    pub deployed_at: chrono::DateTime<chrono::Utc>,
141    pub duration_ms: u64,
142    pub status: DeploymentStatus,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "lowercase")]
147pub enum Environment {
148    Staging,
149    Production,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "snake_case")]
154pub enum DeploymentStatus {
155    Success,
156    Failed,
157    RolledBack,
158}
159
160/// Issue analysis result from IssueAgent
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct IssueAnalysis {
163    pub issue_number: u64,
164    pub issue_type: crate::task::TaskType,
165    pub severity: crate::agent::Severity,
166    pub impact: ImpactLevel,
167    pub assigned_agent: crate::agent::AgentType,
168    pub estimated_duration: u32, // minutes
169    pub dependencies: Vec<String>,
170    pub labels: Vec<String>,
171}
172
173/// Issue Trace Log - complete lifecycle tracking
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct IssueTraceLog {
176    // Identification
177    pub issue_number: u64,
178    pub issue_title: String,
179    pub issue_url: String,
180
181    // Lifecycle tracking
182    pub created_at: chrono::DateTime<chrono::Utc>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub closed_at: Option<chrono::DateTime<chrono::Utc>>,
185    pub current_state: IssueState,
186    pub state_transitions: Vec<StateTransition>,
187
188    // Agent execution tracking
189    pub agent_executions: Vec<AgentExecution>,
190
191    // Task decomposition
192    pub total_tasks: u32,
193    pub completed_tasks: u32,
194    pub failed_tasks: u32,
195
196    // Label tracking
197    pub label_changes: Vec<LabelChange>,
198    pub current_labels: Vec<String>,
199
200    // Quality & metrics
201    pub quality_reports: Vec<QualityReport>,
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub final_quality_score: Option<u8>,
204
205    // Pull Request tracking
206    pub pull_requests: Vec<PRResult>,
207
208    // Deployment tracking
209    pub deployments: Vec<DeploymentResult>,
210
211    // Escalations
212    pub escalations: Vec<EscalationInfo>,
213
214    // Notes & annotations
215    pub notes: Vec<TraceNote>,
216
217    // Metadata
218    pub metadata: IssueMetadata,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct IssueMetadata {
223    pub device_identifier: String,
224    pub session_ids: Vec<String>,
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub total_duration_ms: Option<u64>,
227    pub last_updated: chrono::DateTime<chrono::Utc>,
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    // ========================================================================
235    // IssueStateGithub Tests
236    // ========================================================================
237
238    #[test]
239    fn test_issue_state_github_serialization() {
240        let state = IssueStateGithub::Open;
241        let json = serde_json::to_string(&state).unwrap();
242        assert_eq!(json, "\"open\"");
243
244        let state = IssueStateGithub::Closed;
245        let json = serde_json::to_string(&state).unwrap();
246        assert_eq!(json, "\"closed\"");
247    }
248
249    #[test]
250    fn test_issue_state_github_roundtrip() {
251        let states = vec![IssueStateGithub::Open, IssueStateGithub::Closed];
252
253        for state in states {
254            let json = serde_json::to_string(&state).unwrap();
255            let deserialized: IssueStateGithub = serde_json::from_str(&json).unwrap();
256            assert_eq!(state, deserialized);
257        }
258    }
259
260    // ========================================================================
261    // IssueState Tests
262    // ========================================================================
263
264    #[test]
265    fn test_issue_state_to_label() {
266        assert_eq!(IssueState::Pending.to_label(), "📥 state:pending");
267        assert_eq!(IssueState::Analyzing.to_label(), "🔍 state:analyzing");
268        assert_eq!(IssueState::Implementing.to_label(), "🏗️ state:implementing");
269        assert_eq!(IssueState::Reviewing.to_label(), "👀 state:reviewing");
270        assert_eq!(IssueState::Deploying.to_label(), "🚀 state:deploying");
271        assert_eq!(IssueState::Done.to_label(), "✅ state:done");
272        assert_eq!(IssueState::Blocked.to_label(), "🚫 state:blocked");
273        assert_eq!(IssueState::Failed.to_label(), "❌ state:failed");
274    }
275
276    #[test]
277    fn test_issue_state_serialization() {
278        let state = IssueState::Pending;
279        let json = serde_json::to_string(&state).unwrap();
280        assert_eq!(json, "\"pending\"");
281
282        let state = IssueState::Done;
283        let json = serde_json::to_string(&state).unwrap();
284        assert_eq!(json, "\"done\"");
285    }
286
287    #[test]
288    fn test_issue_state_roundtrip() {
289        let states = vec![
290            IssueState::Pending,
291            IssueState::Analyzing,
292            IssueState::Implementing,
293            IssueState::Reviewing,
294            IssueState::Deploying,
295            IssueState::Done,
296            IssueState::Blocked,
297            IssueState::Failed,
298        ];
299
300        for state in states {
301            let json = serde_json::to_string(&state).unwrap();
302            let deserialized: IssueState = serde_json::from_str(&json).unwrap();
303            assert_eq!(state, deserialized);
304        }
305    }
306
307    // ========================================================================
308    // Issue Tests
309    // ========================================================================
310
311    #[test]
312    fn test_issue_serialization() {
313        let issue = Issue {
314            number: 123,
315            title: "Test issue".to_string(),
316            body: "Issue body".to_string(),
317            state: IssueStateGithub::Open,
318            labels: vec!["bug".to_string(), "priority:high".to_string()],
319            assignee: Some("user123".to_string()),
320            created_at: chrono::Utc::now(),
321            updated_at: chrono::Utc::now(),
322            url: "https://github.com/user/repo/issues/123".to_string(),
323        };
324
325        let json = serde_json::to_string(&issue).unwrap();
326        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
327        assert_eq!(parsed["number"], 123);
328        assert_eq!(parsed["title"], "Test issue");
329        assert_eq!(parsed["state"], "open");
330        assert_eq!(parsed["assignee"], "user123");
331    }
332
333    #[test]
334    fn test_issue_optional_assignee() {
335        let issue = Issue {
336            number: 456,
337            title: "Unassigned issue".to_string(),
338            body: "".to_string(),
339            state: IssueStateGithub::Closed,
340            labels: vec![],
341            assignee: None,
342            created_at: chrono::Utc::now(),
343            updated_at: chrono::Utc::now(),
344            url: "https://github.com/user/repo/issues/456".to_string(),
345        };
346
347        let json = serde_json::to_string(&issue).unwrap();
348        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
349        assert!(parsed.get("assignee").is_none());
350    }
351
352    #[test]
353    fn test_issue_roundtrip() {
354        let issue = Issue {
355            number: 789,
356            title: "Roundtrip test".to_string(),
357            body: "Test".to_string(),
358            state: IssueStateGithub::Open,
359            labels: vec!["test".to_string()],
360            assignee: Some("tester".to_string()),
361            created_at: chrono::Utc::now(),
362            updated_at: chrono::Utc::now(),
363            url: "https://github.com/user/repo/issues/789".to_string(),
364        };
365
366        let json = serde_json::to_string(&issue).unwrap();
367        let deserialized: Issue = serde_json::from_str(&json).unwrap();
368        assert_eq!(issue.number, deserialized.number);
369        assert_eq!(issue.title, deserialized.title);
370        assert_eq!(issue.state, deserialized.state);
371    }
372
373    // ========================================================================
374    // StateTransition Tests
375    // ========================================================================
376
377    #[test]
378    fn test_state_transition_serialization() {
379        let transition = StateTransition {
380            from: IssueState::Pending,
381            to: IssueState::Analyzing,
382            timestamp: chrono::Utc::now(),
383            triggered_by: "CoordinatorAgent".to_string(),
384            reason: Some("Starting analysis".to_string()),
385        };
386
387        let json = serde_json::to_string(&transition).unwrap();
388        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
389        assert_eq!(parsed["from"], "pending");
390        assert_eq!(parsed["to"], "analyzing");
391        assert_eq!(parsed["triggered_by"], "CoordinatorAgent");
392    }
393
394    #[test]
395    fn test_state_transition_roundtrip() {
396        let transition = StateTransition {
397            from: IssueState::Implementing,
398            to: IssueState::Reviewing,
399            timestamp: chrono::Utc::now(),
400            triggered_by: "ReviewAgent".to_string(),
401            reason: None,
402        };
403
404        let json = serde_json::to_string(&transition).unwrap();
405        let deserialized: StateTransition = serde_json::from_str(&json).unwrap();
406        assert_eq!(transition.from, deserialized.from);
407        assert_eq!(transition.to, deserialized.to);
408    }
409
410    // ========================================================================
411    // AgentExecution Tests
412    // ========================================================================
413
414    #[test]
415    fn test_agent_execution_serialization() {
416        let execution = AgentExecution {
417            agent_type: AgentType::CodeGenAgent,
418            task_id: Some("task-001".to_string()),
419            start_time: chrono::Utc::now(),
420            end_time: Some(chrono::Utc::now()),
421            duration_ms: Some(5000),
422            status: AgentStatus::Completed,
423            result: None,
424            error: None,
425        };
426
427        let json = serde_json::to_string(&execution).unwrap();
428        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
429        assert_eq!(parsed["agent_type"], "CodeGenAgent");
430        assert_eq!(parsed["status"], "completed");
431        assert_eq!(parsed["duration_ms"], 5000);
432    }
433
434    #[test]
435    fn test_agent_execution_with_error() {
436        let execution = AgentExecution {
437            agent_type: AgentType::DeploymentAgent,
438            task_id: Some("task-002".to_string()),
439            start_time: chrono::Utc::now(),
440            end_time: Some(chrono::Utc::now()),
441            duration_ms: Some(1000),
442            status: AgentStatus::Failed,
443            result: None,
444            error: Some("Deployment failed".to_string()),
445        };
446
447        let json = serde_json::to_string(&execution).unwrap();
448        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
449        assert_eq!(parsed["error"], "Deployment failed");
450    }
451
452    // ========================================================================
453    // LabelAction Tests
454    // ========================================================================
455
456    #[test]
457    fn test_label_action_serialization() {
458        let action = LabelAction::Added;
459        let json = serde_json::to_string(&action).unwrap();
460        assert_eq!(json, "\"added\"");
461
462        let action = LabelAction::Removed;
463        let json = serde_json::to_string(&action).unwrap();
464        assert_eq!(json, "\"removed\"");
465    }
466
467    #[test]
468    fn test_label_action_roundtrip() {
469        let actions = vec![LabelAction::Added, LabelAction::Removed];
470
471        for action in actions {
472            let json = serde_json::to_string(&action).unwrap();
473            let deserialized: LabelAction = serde_json::from_str(&json).unwrap();
474            assert_eq!(action, deserialized);
475        }
476    }
477
478    // ========================================================================
479    // LabelChange Tests
480    // ========================================================================
481
482    #[test]
483    fn test_label_change_serialization() {
484        let change = LabelChange {
485            timestamp: chrono::Utc::now(),
486            action: LabelAction::Added,
487            label: "bug".to_string(),
488            performed_by: "IssueAgent".to_string(),
489        };
490
491        let json = serde_json::to_string(&change).unwrap();
492        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
493        assert_eq!(parsed["action"], "added");
494        assert_eq!(parsed["label"], "bug");
495    }
496
497    // ========================================================================
498    // PRState Tests
499    // ========================================================================
500
501    #[test]
502    fn test_pr_state_serialization() {
503        let state = PRState::Draft;
504        let json = serde_json::to_string(&state).unwrap();
505        assert_eq!(json, "\"draft\"");
506
507        let state = PRState::Merged;
508        let json = serde_json::to_string(&state).unwrap();
509        assert_eq!(json, "\"merged\"");
510    }
511
512    #[test]
513    fn test_pr_state_roundtrip() {
514        let states = vec![
515            PRState::Draft,
516            PRState::Open,
517            PRState::Merged,
518            PRState::Closed,
519        ];
520
521        for state in states {
522            let json = serde_json::to_string(&state).unwrap();
523            let deserialized: PRState = serde_json::from_str(&json).unwrap();
524            assert_eq!(state, deserialized);
525        }
526    }
527
528    // ========================================================================
529    // PRResult Tests
530    // ========================================================================
531
532    #[test]
533    fn test_pr_result_serialization() {
534        let pr = PRResult {
535            number: 42,
536            url: "https://github.com/user/repo/pull/42".to_string(),
537            state: PRState::Open,
538            created_at: chrono::Utc::now(),
539        };
540
541        let json = serde_json::to_string(&pr).unwrap();
542        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
543        assert_eq!(parsed["number"], 42);
544        assert_eq!(parsed["state"], "open");
545    }
546
547    // ========================================================================
548    // Environment Tests
549    // ========================================================================
550
551    #[test]
552    fn test_environment_serialization() {
553        let env = Environment::Staging;
554        let json = serde_json::to_string(&env).unwrap();
555        assert_eq!(json, "\"staging\"");
556
557        let env = Environment::Production;
558        let json = serde_json::to_string(&env).unwrap();
559        assert_eq!(json, "\"production\"");
560    }
561
562    // ========================================================================
563    // DeploymentStatus Tests
564    // ========================================================================
565
566    #[test]
567    fn test_deployment_status_serialization() {
568        let status = DeploymentStatus::Success;
569        let json = serde_json::to_string(&status).unwrap();
570        assert_eq!(json, "\"success\"");
571
572        let status = DeploymentStatus::RolledBack;
573        let json = serde_json::to_string(&status).unwrap();
574        assert_eq!(json, "\"rolled_back\"");
575    }
576
577    #[test]
578    fn test_deployment_status_roundtrip() {
579        let statuses = vec![
580            DeploymentStatus::Success,
581            DeploymentStatus::Failed,
582            DeploymentStatus::RolledBack,
583        ];
584
585        for status in statuses {
586            let json = serde_json::to_string(&status).unwrap();
587            let deserialized: DeploymentStatus = serde_json::from_str(&json).unwrap();
588            assert_eq!(status, deserialized);
589        }
590    }
591
592    // ========================================================================
593    // DeploymentResult Tests
594    // ========================================================================
595
596    #[test]
597    fn test_deployment_result_serialization() {
598        let deployment = DeploymentResult {
599            environment: Environment::Production,
600            version: "v1.2.3".to_string(),
601            project_id: "project-123".to_string(),
602            deployment_url: "https://app.example.com".to_string(),
603            deployed_at: chrono::Utc::now(),
604            duration_ms: 30000,
605            status: DeploymentStatus::Success,
606        };
607
608        let json = serde_json::to_string(&deployment).unwrap();
609        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
610        assert_eq!(parsed["environment"], "production");
611        assert_eq!(parsed["version"], "v1.2.3");
612        assert_eq!(parsed["status"], "success");
613    }
614
615    // ========================================================================
616    // TraceNote Tests
617    // ========================================================================
618
619    #[test]
620    fn test_trace_note_serialization() {
621        let note = TraceNote {
622            timestamp: chrono::Utc::now(),
623            author: "user123".to_string(),
624            content: "This is a note".to_string(),
625            tags: Some(vec!["important".to_string()]),
626        };
627
628        let json = serde_json::to_string(&note).unwrap();
629        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
630        assert_eq!(parsed["content"], "This is a note");
631        assert_eq!(parsed["tags"][0], "important");
632    }
633
634    // ========================================================================
635    // IssueMetadata Tests
636    // ========================================================================
637
638    #[test]
639    fn test_issue_metadata_serialization() {
640        let metadata = IssueMetadata {
641            device_identifier: "MacBook-Pro".to_string(),
642            session_ids: vec!["session-1".to_string(), "session-2".to_string()],
643            total_duration_ms: Some(120000),
644            last_updated: chrono::Utc::now(),
645        };
646
647        let json = serde_json::to_string(&metadata).unwrap();
648        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
649        assert_eq!(parsed["device_identifier"], "MacBook-Pro");
650        assert_eq!(parsed["total_duration_ms"], 120000);
651    }
652
653    // ========================================================================
654    // IssueTraceLog Tests
655    // ========================================================================
656
657    #[test]
658    fn test_issue_trace_log_structure() {
659        let metadata = IssueMetadata {
660            device_identifier: "test-device".to_string(),
661            session_ids: vec!["session-1".to_string()],
662            total_duration_ms: Some(60000),
663            last_updated: chrono::Utc::now(),
664        };
665
666        let trace_log = IssueTraceLog {
667            issue_number: 100,
668            issue_title: "Test issue".to_string(),
669            issue_url: "https://github.com/user/repo/issues/100".to_string(),
670            created_at: chrono::Utc::now(),
671            closed_at: None,
672            current_state: IssueState::Implementing,
673            state_transitions: vec![],
674            agent_executions: vec![],
675            total_tasks: 5,
676            completed_tasks: 2,
677            failed_tasks: 0,
678            label_changes: vec![],
679            current_labels: vec!["bug".to_string()],
680            quality_reports: vec![],
681            final_quality_score: None,
682            pull_requests: vec![],
683            deployments: vec![],
684            escalations: vec![],
685            notes: vec![],
686            metadata,
687        };
688
689        let json = serde_json::to_string(&trace_log).unwrap();
690        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
691        assert_eq!(parsed["issue_number"], 100);
692        assert_eq!(parsed["current_state"], "implementing");
693        assert_eq!(parsed["total_tasks"], 5);
694        assert_eq!(parsed["completed_tasks"], 2);
695    }
696}