Skip to main content

batuta/stack/
quality_format.rs

1//! Quality Report Formatting
2//!
3//! Text and JSON formatting for stack quality reports.
4
5use anyhow::{anyhow, Result};
6use std::collections::HashMap;
7
8use super::quality::{ComponentQuality, IssueSeverity, StackLayer, StackQualityReport};
9
10/// Layer display order for quality reports.
11const LAYER_ORDER: [StackLayer; 8] = [
12    StackLayer::Compute,
13    StackLayer::Ml,
14    StackLayer::Training,
15    StackLayer::Transpilers,
16    StackLayer::Orchestration,
17    StackLayer::Quality,
18    StackLayer::DataMlops,
19    StackLayer::Presentation,
20];
21
22/// Format a single layer's components as text rows.
23fn format_layer_components(output: &mut String, components: &[&ComponentQuality]) {
24    output.push_str(&format!(
25        "  {:20} {:8} {:8} {:8} {:6} {:7} {:6}\n",
26        "Component", "Rust", "Repo", "README", "Hero", "SQI", "Grade"
27    ));
28    output.push_str(&format!(
29        "  {:20} {:8} {:8} {:8} {:6} {:7} {:6}\n",
30        "─".repeat(20),
31        "─".repeat(8),
32        "─".repeat(8),
33        "─".repeat(8),
34        "─".repeat(6),
35        "─".repeat(7),
36        "─".repeat(6)
37    ));
38
39    for comp in components {
40        let hero_status = if comp.hero_image.valid { "✓" } else { "✗" };
41        output.push_str(&format!(
42            "  {:20} {:>3}/{:<4} {:>3}/{:<4} {:>2}/{:<4} {:^6} {:>6.1} {} {}\n",
43            comp.name,
44            comp.rust_score.value,
45            comp.rust_score.max,
46            comp.repo_score.value,
47            comp.repo_score.max,
48            comp.readme_score.value,
49            comp.readme_score.max,
50            hero_status,
51            comp.sqi,
52            comp.grade.symbol(),
53            comp.grade.icon(),
54        ));
55
56        for issue in &comp.issues {
57            let icon = match issue.severity {
58                IssueSeverity::Error => "└── ❌",
59                IssueSeverity::Warning => "└── ⚠️",
60                IssueSeverity::Info => "└── ℹ️",
61            };
62            output.push_str(&format!("    {} {}\n", icon, issue.message));
63        }
64    }
65}
66
67/// Format quality report as text
68pub fn format_report_text(report: &StackQualityReport) -> String {
69    let mut output = String::new();
70
71    output.push_str("PAIML Stack Quality Matrix\n");
72    output.push_str(&"═".repeat(78));
73    output.push_str("\n\n");
74
75    // Group by layer
76    let mut by_layer: HashMap<StackLayer, Vec<&ComponentQuality>> = HashMap::new();
77    for comp in &report.components {
78        by_layer.entry(comp.layer).or_default().push(comp);
79    }
80
81    for layer in LAYER_ORDER {
82        if let Some(components) = by_layer.get(&layer) {
83            output.push_str(&format!("{}\n", layer.display_name()));
84            output.push_str(&"─".repeat(78));
85            output.push('\n');
86            format_layer_components(&mut output, components);
87            output.push('\n');
88        }
89    }
90
91    // Summary
92    output.push_str(&"═".repeat(78));
93    output.push_str("\nSUMMARY\n");
94    output.push_str(&"═".repeat(78));
95    output.push_str("\n\n");
96
97    output.push_str(&format!(
98        "Quality Distribution:\n  A+  {:3} components ({:.0}%)\n  A   {:3} components ({:.0}%)\n  A-  {:3} components ({:.0}%)\n  <A- {:3} components ({:.0}%)\n\n",
99        report.summary.a_plus_count,
100        (report.summary.a_plus_count as f64 / report.summary.total_components as f64) * 100.0,
101        report.summary.a_count,
102        (report.summary.a_count as f64 / report.summary.total_components as f64) * 100.0,
103        report.summary.a_minus_count,
104        (report.summary.a_minus_count as f64 / report.summary.total_components as f64) * 100.0,
105        report.summary.below_threshold_count,
106        (report.summary.below_threshold_count as f64 / report.summary.total_components as f64) * 100.0,
107    ));
108
109    output.push_str(&format!(
110        "Stack Quality Index: {:.1} ({})\n\n",
111        report.stack_quality_index, report.overall_grade
112    ));
113
114    if report.release_ready {
115        output.push_str("Release Status: ✅ READY\n");
116    } else {
117        output.push_str("Release Status: ❌ BLOCKED\n");
118        output
119            .push_str(&format!("  Blocked components: {}\n", report.blocked_components.join(", ")));
120    }
121
122    if !report.recommendations.is_empty() {
123        output.push_str("\nRecommended Actions:\n");
124        for (i, rec) in report.recommendations.iter().enumerate() {
125            output.push_str(&format!("  {}. {}\n", i + 1, rec));
126        }
127    }
128
129    output
130}
131
132/// Format quality report as JSON
133pub fn format_report_json(report: &StackQualityReport) -> Result<String> {
134    serde_json::to_string_pretty(report).map_err(|e| anyhow!("JSON serialization error: {}", e))
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::stack::hero_image::{HeroImageResult, ImageFormat};
141    use crate::stack::quality::{QualityGrade, Score};
142    use std::path::PathBuf;
143
144    fn create_test_component(
145        name: &str,
146        rust: u32,
147        repo: u32,
148        readme: u32,
149        has_hero: bool,
150    ) -> ComponentQuality {
151        let rust_score = Score::new(rust, 114, QualityGrade::from_rust_project_score(rust));
152        let repo_score = Score::new(repo, 110, QualityGrade::from_repo_score(repo));
153        let readme_score = Score::new(readme, 20, QualityGrade::from_readme_score(readme));
154        let hero = if has_hero {
155            HeroImageResult::found(PathBuf::from("hero.png"), ImageFormat::Png)
156        } else {
157            HeroImageResult::missing()
158        };
159
160        ComponentQuality::new(
161            name,
162            PathBuf::from("/test"),
163            rust_score,
164            repo_score,
165            readme_score,
166            hero,
167        )
168    }
169
170    #[test]
171    fn test_format_report_text() {
172        let components = vec![create_test_component("trueno", 107, 98, 20, true)];
173        let report = StackQualityReport::from_components(components);
174        let text = format_report_text(&report);
175
176        assert!(text.contains("trueno"));
177        assert!(text.contains("PAIML"));
178    }
179
180    #[test]
181    fn test_format_report_json() {
182        let components = vec![create_test_component("trueno", 107, 98, 20, true)];
183        let report = StackQualityReport::from_components(components);
184        let json = format_report_json(&report).expect("unexpected failure");
185
186        assert!(json.contains("trueno"));
187        assert!(json.contains("stack_quality_index"));
188    }
189
190    #[test]
191    fn test_format_report_text_with_layers() {
192        let components = vec![
193            create_test_component("trueno", 107, 98, 20, true), // Compute
194            create_test_component("aprender", 95, 90, 16, true), // ML
195            create_test_component("entrenar", 100, 92, 18, true), // Training
196            create_test_component("depyler", 90, 88, 15, false), // Transpilers
197        ];
198        let report = StackQualityReport::from_components(components);
199        let text = format_report_text(&report);
200
201        // Verify layer headers
202        assert!(text.contains("COMPUTE PRIMITIVES"));
203        assert!(text.contains("ML ALGORITHMS"));
204        assert!(text.contains("TRAINING & INFERENCE"));
205        assert!(text.contains("TRANSPILERS"));
206        assert!(text.contains("SUMMARY"));
207    }
208
209    #[test]
210    fn test_format_report_text_with_issues() {
211        use crate::stack::quality::QualityIssue;
212
213        let mut comp = create_test_component("test", 70, 60, 10, false);
214        comp.issues.push(QualityIssue::new(
215            "low_score",
216            "Score below threshold",
217            IssueSeverity::Error,
218        ));
219        comp.issues.push(QualityIssue::new(
220            "warning",
221            "Missing documentation",
222            IssueSeverity::Warning,
223        ));
224        comp.issues.push(QualityIssue::new(
225            "info",
226            "Consider adding examples",
227            IssueSeverity::Info,
228        ));
229
230        let report = StackQualityReport::from_components(vec![comp]);
231        let text = format_report_text(&report);
232
233        assert!(text.contains("❌"));
234        assert!(text.contains("⚠️"));
235        assert!(text.contains("ℹ️"));
236    }
237
238    #[test]
239    fn test_format_report_text_blocked_components() {
240        let mut comp = create_test_component("blocked", 70, 60, 10, false);
241        comp.release_ready = false;
242        comp.grade = QualityGrade::B;
243
244        let report = StackQualityReport::from_components(vec![comp]);
245        let text = format_report_text(&report);
246
247        assert!(text.contains("BLOCKED"));
248        assert!(text.contains("blocked"));
249    }
250}