miyabi_types/
issue.rs

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