Skip to main content

morph_cli/core/report/
models.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct TransformReport {
9    pub id: String,
10    pub timestamp: i64,
11    pub execution: ExecutionMetadata,
12    pub recipes: Vec<RecipeSummary>,
13    pub files: FileSummary,
14    pub safety: SafetySummary,
15    pub timing: TimingSummary,
16    pub failures: Vec<FailureEntry>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub rollback_session: Option<RollbackInfo>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ExecutionMetadata {
23    pub command: String,
24    pub version: String,
25    pub mode: String,
26    pub project_root: PathBuf,
27    pub target_path: PathBuf,
28    pub allow_risky: bool,
29    pub strict: bool,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RecipeSummary {
34    pub name: String,
35    pub description: String,
36    pub extensions: Vec<String>,
37    pub files_processed: usize,
38    pub files_changed: usize,
39    pub files_skipped: usize,
40    pub unsupported_patterns: usize,
41    #[serde(default)]
42    pub parse_failures: usize,
43    #[serde(default)]
44    pub execution_duration_ms: u64,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct FileSummary {
49    pub total: usize,
50    pub parseable: usize,
51    pub analyses: Vec<FileAnalysis>,
52    pub skipped: Vec<SkippedFile>,
53    pub changed: Vec<ChangedFile>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct FileAnalysis {
58    pub path: PathBuf,
59    pub patterns: Vec<String>,
60    pub confidence: u8,
61    pub safety: String,
62    pub is_safe: bool,
63    #[serde(default)]
64    pub tags: Vec<String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SkippedFile {
69    pub path: PathBuf,
70    pub reason: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ChangedFile {
75    pub path: PathBuf,
76    pub lines_added: usize,
77    pub lines_removed: usize,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub preview: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SafetySummary {
84    pub safe_count: usize,
85    pub risky_count: usize,
86    pub warnings: Vec<String>,
87    pub recommendations: Vec<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct TimingSummary {
92    pub total_ms: u64,
93    pub per_recipe: HashMap<String, RecipeTiming>,
94    pub per_file: HashMap<PathBuf, u64>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct RecipeTiming {
99    pub detect_ms: u64,
100    pub transform_ms: u64,
101    pub total_ms: u64,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct FailureEntry {
106    pub recipe: String,
107    pub stage: String,
108    pub error: String,
109    pub affected_files: Vec<PathBuf>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct RollbackInfo {
114    pub session_id: String,
115    pub created_at: i64,
116    pub file_count: usize,
117}
118
119impl TransformReport {
120    pub fn new(id: String, execution: ExecutionMetadata) -> Self {
121        let timestamp = SystemTime::now()
122            .duration_since(UNIX_EPOCH)
123            .unwrap()
124            .as_secs() as i64;
125        Self {
126            id,
127            timestamp,
128            execution,
129            recipes: Vec::new(),
130            files: FileSummary {
131                total: 0,
132                parseable: 0,
133                analyses: Vec::new(),
134                skipped: Vec::new(),
135                changed: Vec::new(),
136            },
137            safety: SafetySummary {
138                safe_count: 0,
139                risky_count: 0,
140                warnings: Vec::new(),
141                recommendations: Vec::new(),
142            },
143            timing: TimingSummary {
144                total_ms: 0,
145                per_recipe: HashMap::new(),
146                per_file: HashMap::new(),
147            },
148            failures: Vec::new(),
149            rollback_session: None,
150        }
151    }
152
153    #[allow(dead_code)]
154    pub fn total_changes(&self) -> usize {
155        self.files.changed.len()
156    }
157
158    #[allow(dead_code)]
159    pub fn total_skipped(&self) -> usize {
160        self.files.skipped.len()
161    }
162
163    #[allow(dead_code)]
164    pub fn has_failures(&self) -> bool {
165        !self.failures.is_empty()
166    }
167}
168
169impl Default for TransformReport {
170    fn default() -> Self {
171        Self::new(
172            generate_report_id(),
173            ExecutionMetadata {
174                command: String::new(),
175                version: env!("CARGO_PKG_VERSION").to_string(),
176                mode: String::new(),
177                project_root: PathBuf::new(),
178                target_path: PathBuf::new(),
179                allow_risky: false,
180                strict: false,
181            },
182        )
183    }
184}
185
186fn generate_report_id() -> String {
187    let timestamp = SystemTime::now()
188        .duration_since(UNIX_EPOCH)
189        .unwrap()
190        .as_secs();
191    format!("morph-report-{}", timestamp)
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_report_creation() {
200        let report = TransformReport::default();
201        assert!(!report.id.is_empty());
202    }
203
204    #[test]
205    fn test_report_totals() {
206        let mut report = TransformReport::default();
207        report.files.changed.push(ChangedFile {
208            path: PathBuf::from("test.js"),
209            lines_added: 10,
210            lines_removed: 5,
211            preview: None,
212        });
213        assert_eq!(report.total_changes(), 1);
214        assert_eq!(report.total_skipped(), 0);
215        assert!(!report.has_failures());
216    }
217
218    #[test]
219    fn test_serialization() {
220        let report = TransformReport::default();
221        let json = serde_json::to_string(&report).unwrap();
222        assert!(json.contains("morph-report-"));
223        let deserialized: TransformReport = serde_json::from_str(&json).unwrap();
224        assert_eq!(report.id, deserialized.id);
225    }
226}