1use crate::types::{ProjectAnalysis, WorkflowState};
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct MigrationReport {
10 pub project_name: String,
11 pub analysis: ProjectAnalysis,
12 pub workflow: WorkflowState,
13 pub timestamp: chrono::DateTime<chrono::Utc>,
14}
15
16impl MigrationReport {
17 pub fn new(project_name: String, analysis: ProjectAnalysis, workflow: WorkflowState) -> Self {
18 Self { project_name, analysis, workflow, timestamp: chrono::Utc::now() }
19 }
20
21 pub fn to_html(&self) -> String {
23 let mut html = String::new();
24
25 html.push_str("<!DOCTYPE html>\n");
27 html.push_str("<html lang=\"en\">\n");
28 html.push_str("<head>\n");
29 html.push_str(" <meta charset=\"UTF-8\">\n");
30 html.push_str(
31 " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n",
32 );
33 html.push_str(&format!(" <title>Migration Report - {}</title>\n", self.project_name));
34 html.push_str(" <style>\n");
35 html.push_str(include_str!("report_style.css"));
36 html.push_str(" </style>\n");
37 html.push_str("</head>\n");
38 html.push_str("<body>\n");
39
40 html.push_str(&format!(
42 "<header><h1>Migration Report: {}</h1><p>Generated: {}</p></header>\n",
43 self.project_name,
44 format_timestamp(self.timestamp, "%Y-%m-%d %H:%M:%S UTC")
45 ));
46
47 html.push_str("<section class=\"summary\">\n");
49 html.push_str("<h2>Summary</h2>\n");
50 html.push_str("<div class=\"stats\">\n");
51 html.push_str(&format!(
52 "<div class=\"stat\"><span class=\"label\">Total Files</span><span class=\"value\">{}</span></div>\n",
53 self.analysis.total_files
54 ));
55 html.push_str(&format!(
56 "<div class=\"stat\"><span class=\"label\">Total Lines</span><span class=\"value\">{}</span></div>\n",
57 self.analysis.total_lines
58 ));
59 if let Some(lang) = &self.analysis.primary_language {
60 html.push_str(&format!(
61 "<div class=\"stat\"><span class=\"label\">Primary Language</span><span class=\"value\">{}</span></div>\n",
62 lang
63 ));
64 }
65 if let Some(score) = self.analysis.tdg_score {
66 html.push_str(&format!(
67 "<div class=\"stat\"><span class=\"label\">TDG Score</span><span class=\"value\">{}</span></div>\n",
68 format_tdg_score(score)
69 ));
70 }
71 html.push_str("</div>\n");
72 html.push_str("</section>\n");
73
74 if !self.analysis.languages.is_empty() {
76 html.push_str("<section class=\"languages\">\n");
77 html.push_str("<h2>Languages</h2>\n");
78 html.push_str("<table>\n");
79 html.push_str("<thead><tr><th>Language</th><th>Files</th><th>Lines</th><th>Percentage</th></tr></thead>\n");
80 html.push_str("<tbody>\n");
81 for lang_stat in &self.analysis.languages {
82 html.push_str(&format!(
83 "<tr><td>{}</td><td>{}</td><td>{}</td><td>{:.1}%</td></tr>\n",
84 lang_stat.language,
85 lang_stat.file_count,
86 lang_stat.line_count,
87 lang_stat.percentage
88 ));
89 }
90 html.push_str("</tbody>\n");
91 html.push_str("</table>\n");
92 html.push_str("</section>\n");
93 }
94
95 if !self.analysis.dependencies.is_empty() {
97 html.push_str("<section class=\"dependencies\">\n");
98 html.push_str("<h2>Dependencies</h2>\n");
99 html.push_str("<ul>\n");
100 for dep in &self.analysis.dependencies {
101 html.push_str(&format!(
102 "<li><strong>{}</strong>{} - {:?}</li>\n",
103 dep.manager,
104 format_dep_count(dep.count),
105 dep.file_path
106 ));
107 }
108 html.push_str("</ul>\n");
109 html.push_str("</section>\n");
110 }
111
112 html.push_str("<section class=\"workflow\">\n");
114 html.push_str("<h2>Workflow Progress</h2>\n");
115 html.push_str(&format!(
116 "<p class=\"progress\">{:.0}% complete</p>\n",
117 self.workflow.progress_percentage()
118 ));
119 html.push_str("<table>\n");
120 html.push_str("<thead><tr><th>Phase</th><th>Status</th><th>Started</th><th>Completed</th></tr></thead>\n");
121 html.push_str("<tbody>\n");
122 for phase in crate::types::WorkflowPhase::all() {
123 if let Some(info) = self.workflow.phases.get(&phase) {
124 let status_class = match info.status {
125 crate::types::PhaseStatus::Completed => "completed",
126 crate::types::PhaseStatus::InProgress => "in-progress",
127 crate::types::PhaseStatus::Failed => "failed",
128 crate::types::PhaseStatus::NotStarted => "not-started",
129 };
130 html.push_str(&format!("<tr class=\"{}\">\n", status_class));
131 html.push_str(&format!("<td>{}</td>\n", phase));
132 html.push_str(&format!("<td>{}</td>\n", info.status));
133 html.push_str(&format!(
134 "<td>{}</td>\n",
135 format_timestamp_or_dash(info.started_at, "%Y-%m-%d %H:%M:%S")
136 ));
137 html.push_str(&format!(
138 "<td>{}</td>\n",
139 format_timestamp_or_dash(info.completed_at, "%Y-%m-%d %H:%M:%S")
140 ));
141 html.push_str("</tr>\n");
142 }
143 }
144 html.push_str("</tbody>\n");
145 html.push_str("</table>\n");
146 html.push_str("</section>\n");
147
148 html.push_str("<section class=\"recommendations\">\n");
150 html.push_str("<h2>Recommendations</h2>\n");
151 html.push_str("<ul>\n");
152 if let Some(transpiler) = self.analysis.recommend_transpiler() {
153 html.push_str(&format!(
154 "<li>Use <strong>{}</strong> for transpilation</li>\n",
155 transpiler
156 ));
157 }
158 if self.analysis.has_ml_dependencies() {
159 html.push_str("<li>Consider <strong>Aprender</strong> for ML algorithms and <strong>Realizar</strong> for inference</li>\n");
160 }
161 if needs_refactoring(self.analysis.tdg_score) {
162 html.push_str("<li>TDG score below 85 - consider refactoring before migration</li>\n");
163 }
164 html.push_str("</ul>\n");
165 html.push_str("</section>\n");
166
167 html.push_str("<footer>\n");
169 html.push_str("<p>Generated by Batuta - Sovereign AI Stack</p>\n");
170 html.push_str(
171 "<p><a href=\"https://github.com/paiml/Batuta\">github.com/paiml/Batuta</a></p>\n",
172 );
173 html.push_str("</footer>\n");
174
175 html.push_str("</body>\n");
176 html.push_str("</html>\n");
177
178 html
179 }
180
181 pub fn to_markdown(&self) -> String {
183 let mut md = String::new();
184
185 md.push_str(&format!("# Migration Report: {}\n\n", self.project_name));
187 md.push_str(&format!(
188 "**Generated:** {}\n\n",
189 format_timestamp(self.timestamp, "%Y-%m-%d %H:%M:%S UTC")
190 ));
191
192 md.push_str("## Summary\n\n");
194 md.push_str(&format!("- **Total Files:** {}\n", self.analysis.total_files));
195 md.push_str(&format!("- **Total Lines:** {}\n", self.analysis.total_lines));
196 if let Some(lang) = &self.analysis.primary_language {
197 md.push_str(&format!("- **Primary Language:** {}\n", lang));
198 }
199 if let Some(score) = self.analysis.tdg_score {
200 md.push_str(&format!("- **TDG Score:** {}\n", format_tdg_score(score)));
201 }
202 md.push('\n');
203
204 if !self.analysis.languages.is_empty() {
206 md.push_str("## Languages\n\n");
207 md.push_str("| Language | Files | Lines | Percentage |\n");
208 md.push_str("|----------|-------|-------|------------|\n");
209 for lang_stat in &self.analysis.languages {
210 md.push_str(&format!(
211 "| {} | {} | {} | {:.1}% |\n",
212 lang_stat.language,
213 lang_stat.file_count,
214 lang_stat.line_count,
215 lang_stat.percentage
216 ));
217 }
218 md.push('\n');
219 }
220
221 if !self.analysis.dependencies.is_empty() {
223 md.push_str("## Dependencies\n\n");
224 for dep in &self.analysis.dependencies {
225 md.push_str(&format!(
226 "- **{}**{} - `{:?}`\n",
227 dep.manager,
228 format_dep_count(dep.count),
229 dep.file_path
230 ));
231 }
232 md.push('\n');
233 }
234
235 md.push_str("## Workflow Progress\n\n");
237 md.push_str(&format!(
238 "**Overall:** {:.0}% complete\n\n",
239 self.workflow.progress_percentage()
240 ));
241 md.push_str("| Phase | Status | Started | Completed |\n");
242 md.push_str("|-------|--------|---------|----------|\n");
243 for phase in crate::types::WorkflowPhase::all() {
244 if let Some(info) = self.workflow.phases.get(&phase) {
245 md.push_str(&format!(
246 "| {} | {} | {} | {} |\n",
247 phase,
248 info.status,
249 format_timestamp_or_dash(info.started_at, "%Y-%m-%d %H:%M"),
250 format_timestamp_or_dash(info.completed_at, "%Y-%m-%d %H:%M")
251 ));
252 }
253 }
254 md.push('\n');
255
256 md.push_str("## Recommendations\n\n");
258 if let Some(transpiler) = self.analysis.recommend_transpiler() {
259 md.push_str(&format!("- Use **{}** for transpilation\n", transpiler));
260 }
261 if self.analysis.has_ml_dependencies() {
262 md.push_str(
263 "- Consider **Aprender** for ML algorithms and **Realizar** for inference\n",
264 );
265 }
266 if needs_refactoring(self.analysis.tdg_score) {
267 md.push_str("- TDG score below 85 - consider refactoring before migration\n");
268 }
269 md.push('\n');
270
271 md.push_str("---\n\n");
273 md.push_str("*Generated by Batuta - Sovereign AI Stack* \n");
274 md.push_str("https://github.com/paiml/Batuta\n");
275
276 md
277 }
278
279 pub fn to_json(&self) -> Result<String> {
281 Ok(serde_json::to_string_pretty(self)?)
282 }
283
284 pub fn to_text(&self) -> String {
286 let mut text = String::new();
287
288 text.push_str(&format!("MIGRATION REPORT: {}\n", self.project_name));
290 text.push_str(&format!(
291 "Generated: {}\n",
292 format_timestamp(self.timestamp, "%Y-%m-%d %H:%M:%S UTC")
293 ));
294 text.push_str(&"=".repeat(80));
295 text.push_str("\n\n");
296
297 text.push_str("SUMMARY\n");
299 text.push_str(&"-".repeat(80));
300 text.push('\n');
301 text.push_str(&format!("Total Files: {}\n", self.analysis.total_files));
302 text.push_str(&format!("Total Lines: {}\n", self.analysis.total_lines));
303 if let Some(lang) = &self.analysis.primary_language {
304 text.push_str(&format!("Primary Language: {}\n", lang));
305 }
306 if let Some(score) = self.analysis.tdg_score {
307 text.push_str(&format!("TDG Score: {}\n", format_tdg_score(score)));
308 }
309 text.push('\n');
310
311 if !self.analysis.languages.is_empty() {
313 text.push_str("LANGUAGES\n");
314 text.push_str(&"-".repeat(80));
315 text.push('\n');
316 for lang_stat in &self.analysis.languages {
317 text.push_str(&format!(
318 "{:15} {:8} files {:10} lines {:5.1}%\n",
319 format!("{}", lang_stat.language),
320 lang_stat.file_count,
321 lang_stat.line_count,
322 lang_stat.percentage
323 ));
324 }
325 text.push('\n');
326 }
327
328 if !self.analysis.dependencies.is_empty() {
330 text.push_str("DEPENDENCIES\n");
331 text.push_str(&"-".repeat(80));
332 text.push('\n');
333 for dep in &self.analysis.dependencies {
334 text.push_str(&format!("{}{}\n", dep.manager, format_dep_count(dep.count)));
335 text.push_str(&format!(" File: {:?}\n", dep.file_path));
336 }
337 text.push('\n');
338 }
339
340 text.push_str("WORKFLOW PROGRESS\n");
342 text.push_str(&"-".repeat(80));
343 text.push('\n');
344 text.push_str(&format!(
345 "Overall: {:.0}% complete\n\n",
346 self.workflow.progress_percentage()
347 ));
348 for phase in crate::types::WorkflowPhase::all() {
349 if let Some(info) = self.workflow.phases.get(&phase) {
350 text.push_str(&format!(
351 "{:15} {:12}",
352 format!("{}", phase),
353 format!("{}", info.status)
354 ));
355 if let Some(started) = info.started_at {
356 text.push_str(&format!(
357 " Started: {}",
358 format_timestamp(started, "%Y-%m-%d %H:%M")
359 ));
360 }
361 if let Some(completed) = info.completed_at {
362 text.push_str(&format!(
363 " Completed: {}",
364 format_timestamp(completed, "%Y-%m-%d %H:%M")
365 ));
366 }
367 text.push('\n');
368 }
369 }
370 text.push('\n');
371
372 text.push_str("RECOMMENDATIONS\n");
374 text.push_str(&"-".repeat(80));
375 text.push('\n');
376 if let Some(transpiler) = self.analysis.recommend_transpiler() {
377 text.push_str(&format!("• Use {} for transpilation\n", transpiler));
378 }
379 if self.analysis.has_ml_dependencies() {
380 text.push_str("• Consider Aprender for ML algorithms and Realizar for inference\n");
381 }
382 if needs_refactoring(self.analysis.tdg_score) {
383 text.push_str("• TDG score below 85 - consider refactoring before migration\n");
384 }
385 text.push('\n');
386
387 text
388 }
389
390 pub fn save(&self, path: &Path, format: ReportFormat) -> Result<()> {
392 let content = match format {
393 ReportFormat::Html => self.to_html(),
394 ReportFormat::Markdown => self.to_markdown(),
395 ReportFormat::Json => self.to_json()?,
396 ReportFormat::Text => self.to_text(),
397 };
398
399 std::fs::write(path, content)?;
400 Ok(())
401 }
402}
403
404#[derive(Debug, Clone, Copy)]
405pub enum ReportFormat {
406 Html,
407 Markdown,
408 Json,
409 Text,
410}
411
412fn get_tdg_grade(score: f64) -> &'static str {
413 const GRADES: &[(f64, &str)] =
414 &[(95.0, "A+"), (90.0, "A"), (85.0, "B+"), (80.0, "B"), (70.0, "C")];
415 GRADES.iter().find(|(threshold, _)| score >= *threshold).map_or("D", |(_, grade)| grade)
416}
417
418fn format_tdg_score(score: f64) -> String {
419 format!("{:.1}/100 ({})", score, get_tdg_grade(score))
420}
421
422fn format_dep_count(count: Option<usize>) -> String {
423 count.map_or_else(String::new, |c| format!(" ({} packages)", c))
424}
425
426fn format_timestamp(dt: chrono::DateTime<chrono::Utc>, fmt: &str) -> String {
427 dt.format(fmt).to_string()
428}
429
430fn format_timestamp_or_dash(dt: Option<chrono::DateTime<chrono::Utc>>, fmt: &str) -> String {
431 dt.map_or_else(|| "-".to_string(), |t| t.format(fmt).to_string())
432}
433
434fn needs_refactoring(tdg_score: Option<f64>) -> bool {
435 tdg_score.is_some_and(|s| s < 85.0)
436}
437
438#[cfg(test)]
439#[path = "report_tests.rs"]
440mod tests;