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}