Skip to main content

batuta/
report.rs

1/// Report generation for migration analysis
2use crate::types::{ProjectAnalysis, WorkflowState};
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6
7/// Migration report data
8#[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    /// Generate HTML report
22    pub fn to_html(&self) -> String {
23        let mut html = String::new();
24
25        // HTML header
26        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        // Header
41        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        // Summary section
48        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        // Languages section
75        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        // Dependencies section
96        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        // Workflow progress section
113        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        // Recommendations section
149        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        // Footer
168        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    /// Generate Markdown report
182    pub fn to_markdown(&self) -> String {
183        let mut md = String::new();
184
185        // Header
186        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        // Summary
193        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        // Languages
205        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        // Dependencies
222        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        // Workflow
236        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        // Recommendations
257        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        // Footer
272        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    /// Generate JSON report
280    pub fn to_json(&self) -> Result<String> {
281        Ok(serde_json::to_string_pretty(self)?)
282    }
283
284    /// Generate plain text report
285    pub fn to_text(&self) -> String {
286        let mut text = String::new();
287
288        // Header
289        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        // Summary
298        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        // Languages
312        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        // Dependencies
329        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        // Workflow
341        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        // Recommendations
373        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    /// Save report to file
391    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;