organizational_intelligence_plugin/
viz.rs1#[cfg(feature = "viz")]
6use trueno_viz::output::{TerminalEncoder, TerminalMode};
7#[cfg(feature = "viz")]
8use trueno_viz::plots::{BinStrategy, Heatmap, Histogram};
9
10#[cfg(test)]
11use crate::citl::TrainingSource;
12use crate::training::TrainingExample;
13
14pub struct DefectDistribution {
16 pub categories: Vec<String>,
17 pub counts: Vec<u32>,
18 pub percentages: Vec<f32>,
19}
20
21impl DefectDistribution {
22 pub fn from_examples(examples: &[TrainingExample]) -> Self {
24 let mut category_counts: std::collections::HashMap<String, u32> =
25 std::collections::HashMap::new();
26
27 for example in examples {
28 let label_str = format!("{:?}", example.label);
29 *category_counts.entry(label_str).or_insert(0) += 1;
30 }
31
32 let total = examples.len() as f32;
33 let mut items: Vec<_> = category_counts.into_iter().collect();
34 items.sort_by(|a, b| b.1.cmp(&a.1)); let categories: Vec<String> = items.iter().map(|(k, _)| k.clone()).collect();
37 let counts: Vec<u32> = items.iter().map(|(_, v)| *v).collect();
38 let percentages: Vec<f32> = counts.iter().map(|c| (*c as f32 / total) * 100.0).collect();
39
40 Self {
41 categories,
42 counts,
43 percentages,
44 }
45 }
46
47 pub fn to_ascii(&self, width: usize) -> String {
49 let max_count = self.counts.iter().max().copied().unwrap_or(1) as f32;
50 let max_label_len = self.categories.iter().map(|s| s.len()).max().unwrap_or(15);
51
52 let mut output = String::new();
53 for (i, category) in self.categories.iter().enumerate() {
54 let count = self.counts[i];
55 let pct = self.percentages[i];
56 let bar_len =
57 ((count as f32 / max_count) * (width - max_label_len - 15) as f32) as usize;
58 let bar: String = "█".repeat(bar_len.max(1));
59
60 output.push_str(&format!(
61 "{:width$} {:>5} {:>5.1}%\n",
62 category,
63 bar,
64 pct,
65 width = max_label_len
66 ));
67 }
68 output
69 }
70}
71
72pub struct ConfidenceDistribution {
74 pub values: Vec<f32>,
75 pub min: f32,
76 pub max: f32,
77 pub mean: f32,
78}
79
80impl ConfidenceDistribution {
81 pub fn from_examples(examples: &[TrainingExample]) -> Self {
83 let values: Vec<f32> = examples.iter().map(|e| e.confidence).collect();
84 let min = values.iter().cloned().fold(f32::INFINITY, f32::min);
85 let max = values.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
86 let mean = values.iter().sum::<f32>() / values.len() as f32;
87
88 Self {
89 values,
90 min,
91 max,
92 mean,
93 }
94 }
95
96 pub fn to_ascii(&self, bins: usize, height: usize) -> String {
98 let range = self.max - self.min;
99 let bin_width = range / bins as f32;
100
101 let mut bin_counts = vec![0u32; bins];
103 for &v in &self.values {
104 let bin_idx = ((v - self.min) / bin_width).floor() as usize;
105 let bin_idx = bin_idx.min(bins - 1);
106 bin_counts[bin_idx] += 1;
107 }
108
109 let max_count = *bin_counts.iter().max().unwrap_or(&1) as f32;
110
111 let mut output = String::new();
113 for row in (0..height).rev() {
114 let threshold = (row as f32 / height as f32) * max_count;
115 output.push('│');
116 for &count in &bin_counts {
117 if count as f32 >= threshold {
118 output.push_str("██");
119 } else {
120 output.push_str(" ");
121 }
122 }
123 output.push('\n');
124 }
125
126 output.push('└');
128 output.push_str(&"──".repeat(bins));
129 output.push('\n');
130 output.push_str(&format!(
131 " {:.2}{} {:.2}\n",
132 self.min,
133 " ".repeat(bins * 2 - 10),
134 self.max
135 ));
136 output.push_str(&format!(" Mean: {:.2}\n", self.mean));
137
138 output
139 }
140}
141
142#[derive(Default)]
144pub struct CrossRepoMatrix {
145 pub repos: Vec<String>,
146 pub categories: Vec<String>,
147 pub matrix: Vec<Vec<f32>>, }
149
150impl CrossRepoMatrix {
151 #[must_use]
153 pub fn new() -> Self {
154 Self::default()
155 }
156
157 pub fn add_repo(&mut self, repo_name: &str, dist: &DefectDistribution) {
159 self.repos.push(repo_name.to_string());
160
161 for cat in &dist.categories {
163 if !self.categories.contains(cat) {
164 self.categories.push(cat.clone());
165 self.matrix.push(vec![0.0; self.repos.len() - 1]);
166 }
167 }
168
169 for row in &mut self.matrix {
171 row.push(0.0);
172 }
173
174 let repo_idx = self.repos.len() - 1;
176 for (i, cat) in dist.categories.iter().enumerate() {
177 if let Some(cat_idx) = self.categories.iter().position(|c| c == cat) {
178 self.matrix[cat_idx][repo_idx] = dist.percentages[i];
179 }
180 }
181 }
182
183 pub fn to_ascii(&self) -> String {
185 let max_cat_len = self.categories.iter().map(|s| s.len()).max().unwrap_or(15);
186 let col_width = 8;
187
188 let mut output = String::new();
189
190 output.push_str(&" ".repeat(max_cat_len + 1));
192 for repo in &self.repos {
193 output.push_str(&format!(
194 "{:>width$}",
195 &repo[..repo.len().min(col_width)],
196 width = col_width
197 ));
198 }
199 output.push('\n');
200
201 for (cat_idx, category) in self.categories.iter().enumerate() {
203 output.push_str(&format!("{:width$} ", category, width = max_cat_len));
204 for &pct in &self.matrix[cat_idx] {
205 let block = match pct {
206 p if p >= 40.0 => "███████",
207 p if p >= 20.0 => "█████░░",
208 p if p >= 10.0 => "███░░░░",
209 p if p >= 5.0 => "██░░░░░",
210 p if p > 0.0 => "█░░░░░░",
211 _ => "░░░░░░░",
212 };
213 output.push_str(&format!(" {}", block));
214 }
215 output.push('\n');
216 }
217
218 output
219 }
220}
221
222#[cfg(feature = "viz")]
223pub fn render_distribution_heatmap(matrix: &CrossRepoMatrix) -> Result<(), anyhow::Error> {
225 let rows = matrix.categories.len();
227 let cols = matrix.repos.len();
228 let data: Vec<f32> = matrix.matrix.iter().flatten().copied().collect();
229
230 let heatmap = Heatmap::new().data(&data, rows, cols).build()?;
231
232 let fb = heatmap.to_framebuffer()?;
233 TerminalEncoder::new()
234 .mode(TerminalMode::UnicodeHalfBlock)
235 .width(80)
236 .print(&fb);
237
238 Ok(())
239}
240
241#[cfg(feature = "viz")]
242pub fn render_confidence_histogram(dist: &ConfidenceDistribution) -> Result<(), anyhow::Error> {
244 let histogram = Histogram::new()
245 .data(&dist.values)
246 .bins(BinStrategy::Fixed(20))
247 .build()?;
248
249 let fb = histogram.to_framebuffer()?;
250 TerminalEncoder::new()
251 .mode(TerminalMode::UnicodeHalfBlock)
252 .width(80)
253 .print(&fb);
254
255 Ok(())
256}
257
258pub fn print_summary_report(
260 repo_name: &str,
261 dist: &DefectDistribution,
262 confidence: &ConfidenceDistribution,
263) {
264 println!();
265 println!("Organizational Intelligence Report: {}", repo_name);
266 println!("{}", "═".repeat(50));
267 println!();
268 println!(
269 "Defect Distribution ({} commits analyzed)",
270 dist.counts.iter().sum::<u32>()
271 );
272 println!("{}", "─".repeat(40));
273 println!("{}", dist.to_ascii(50));
274 println!();
275 println!("Confidence Distribution");
276 println!("{}", "─".repeat(40));
277 println!("{}", confidence.to_ascii(20, 8));
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use crate::classifier::DefectCategory;
284
285 fn make_example(label: DefectCategory, confidence: f32) -> TrainingExample {
286 TrainingExample {
287 message: "test fix".to_string(),
288 label,
289 confidence,
290 commit_hash: "abc123".to_string(),
291 author: "test".to_string(),
292 timestamp: 0,
293 lines_added: 10,
294 lines_removed: 5,
295 files_changed: 1,
296 error_code: None,
297 clippy_lint: None,
298 has_suggestion: false,
299 suggestion_applicability: None,
300 source: TrainingSource::CommitMessage,
301 }
302 }
303
304 #[test]
305 fn test_defect_distribution() {
306 let examples = vec![
307 make_example(DefectCategory::ASTTransform, 0.9),
308 make_example(DefectCategory::ASTTransform, 0.85),
309 make_example(DefectCategory::OwnershipBorrow, 0.8),
310 ];
311
312 let dist = DefectDistribution::from_examples(&examples);
313 assert_eq!(dist.categories.len(), 2);
314 assert_eq!(dist.counts[0], 2); let ascii = dist.to_ascii(40);
317 assert!(ascii.contains("ASTTransform"));
318 }
319
320 #[test]
321 fn test_confidence_distribution() {
322 let examples = vec![
323 make_example(DefectCategory::ASTTransform, 0.7),
324 make_example(DefectCategory::ASTTransform, 0.9),
325 ];
326
327 let dist = ConfidenceDistribution::from_examples(&examples);
328 assert!((dist.mean - 0.8).abs() < 0.01);
329 assert!((dist.min - 0.7).abs() < 0.01);
330 assert!((dist.max - 0.9).abs() < 0.01);
331 }
332
333 #[test]
334 fn test_cross_repo_matrix() {
335 let mut matrix = CrossRepoMatrix::new();
336
337 let dist1 = DefectDistribution {
338 categories: vec!["ASTTransform".to_string(), "Security".to_string()],
339 counts: vec![50, 10],
340 percentages: vec![50.0, 10.0],
341 };
342
343 let dist2 = DefectDistribution {
344 categories: vec!["ASTTransform".to_string(), "Memory".to_string()],
345 counts: vec![40, 20],
346 percentages: vec![40.0, 20.0],
347 };
348
349 matrix.add_repo("depyler", &dist1);
350 matrix.add_repo("bashrs", &dist2);
351
352 assert_eq!(matrix.repos.len(), 2);
353 assert_eq!(matrix.categories.len(), 3); let ascii = matrix.to_ascii();
356 assert!(ascii.contains("depyler"));
357 assert!(ascii.contains("bashrs"));
358 }
359}