1use anyhow::{anyhow, Result};
6use std::collections::HashMap;
7
8use super::quality::{ComponentQuality, IssueSeverity, StackLayer, StackQualityReport};
9
10const 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
22fn 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
67pub 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 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 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
132pub 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), create_test_component("aprender", 95, 90, 16, true), create_test_component("entrenar", 100, 92, 18, true), create_test_component("depyler", 90, 88, 15, false), ];
198 let report = StackQualityReport::from_components(components);
199 let text = format_report_text(&report);
200
201 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}