organizational_intelligence_plugin/
report.rs

1// Report generation module
2// Toyota Way: Start simple, deliver value
3
4use crate::classifier::DefectCategory;
5use anyhow::Result;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8use tokio::fs;
9use tracing::{debug, info};
10
11/// Analysis metadata
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AnalysisMetadata {
14    pub organization: String,
15    pub analysis_date: String,
16    pub repositories_analyzed: usize,
17    pub commits_analyzed: usize,
18    pub analyzer_version: String,
19}
20
21/// Quality signals aggregated for a defect category
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct QualitySignals {
24    /// Average TDG score across all instances
25    pub avg_tdg_score: Option<f32>,
26    /// Maximum TDG score seen
27    pub max_tdg_score: Option<f32>,
28    /// Average cyclomatic complexity
29    pub avg_complexity: Option<f32>,
30    /// Average test coverage (0.0 to 1.0)
31    pub avg_test_coverage: Option<f32>,
32    /// Number of SATD (TODO/FIXME/HACK) markers found
33    pub satd_instances: usize,
34    /// Average lines changed per commit
35    pub avg_lines_changed: f32,
36    /// Number of files changed per commit on average
37    pub avg_files_per_commit: f32,
38}
39
40impl Default for QualitySignals {
41    fn default() -> Self {
42        Self {
43            avg_tdg_score: None,
44            max_tdg_score: None,
45            avg_complexity: None,
46            avg_test_coverage: None,
47            satd_instances: 0,
48            avg_lines_changed: 0.0,
49            avg_files_per_commit: 0.0,
50        }
51    }
52}
53
54/// Enhanced defect instance with quality metrics
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct DefectInstance {
57    pub commit_hash: String,
58    pub message: String,
59    pub author: String,
60    pub timestamp: i64,
61    /// Number of files affected
62    pub files_affected: usize,
63    /// Lines added in this commit
64    pub lines_added: usize,
65    /// Lines removed in this commit
66    pub lines_removed: usize,
67}
68
69/// Defect pattern information
70/// Represents aggregated statistics for a defect category
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct DefectPattern {
73    pub category: DefectCategory,
74    pub frequency: usize,
75    pub confidence: f32,
76    /// Quality signals for this defect category
77    pub quality_signals: QualitySignals,
78    /// Enhanced examples with metrics
79    pub examples: Vec<DefectInstance>,
80}
81
82/// Complete analysis report
83/// Following specification Section 6: YAML Schema
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct AnalysisReport {
86    pub version: String,
87    pub metadata: AnalysisMetadata,
88    pub defect_patterns: Vec<DefectPattern>,
89}
90
91/// Report generator
92/// Phase 1: Basic YAML output generation
93pub struct ReportGenerator;
94
95impl ReportGenerator {
96    /// Create a new report generator
97    ///
98    /// # Examples
99    /// ```
100    /// use organizational_intelligence_plugin::report::ReportGenerator;
101    ///
102    /// let generator = ReportGenerator::new();
103    /// ```
104    pub fn new() -> Self {
105        Self
106    }
107
108    /// Convert report to YAML string
109    ///
110    /// # Arguments
111    /// * `report` - The analysis report to serialize
112    ///
113    /// # Errors
114    /// Returns error if serialization fails
115    ///
116    /// # Examples
117    /// ```
118    /// use organizational_intelligence_plugin::report::{
119    ///     ReportGenerator, AnalysisReport, AnalysisMetadata
120    /// };
121    ///
122    /// let generator = ReportGenerator::new();
123    /// let metadata = AnalysisMetadata {
124    ///     organization: "test-org".to_string(),
125    ///     analysis_date: "2025-11-15T00:00:00Z".to_string(),
126    ///     repositories_analyzed: 10,
127    ///     commits_analyzed: 100,
128    ///     analyzer_version: "0.1.0".to_string(),
129    /// };
130    ///
131    /// let report = AnalysisReport {
132    ///     version: "1.0".to_string(),
133    ///     metadata,
134    ///     defect_patterns: vec![],
135    /// };
136    ///
137    /// let yaml = generator.to_yaml(&report).expect("Should serialize");
138    /// assert!(yaml.contains("version"));
139    /// ```
140    pub fn to_yaml(&self, report: &AnalysisReport) -> Result<String> {
141        debug!("Serializing report to YAML");
142        let yaml = serde_yaml::to_string(report)?;
143        Ok(yaml)
144    }
145
146    /// Write report to file
147    ///
148    /// # Arguments
149    /// * `report` - The analysis report to write
150    /// * `path` - Path to output file
151    ///
152    /// # Errors
153    /// Returns error if:
154    /// - Serialization fails
155    /// - File write fails
156    /// - Path is invalid
157    ///
158    /// # Examples
159    /// ```no_run
160    /// use organizational_intelligence_plugin::report::{
161    ///     ReportGenerator, AnalysisReport, AnalysisMetadata
162    /// };
163    /// use std::path::PathBuf;
164    ///
165    /// # async fn example() -> Result<(), anyhow::Error> {
166    /// let generator = ReportGenerator::new();
167    /// let metadata = AnalysisMetadata {
168    ///     organization: "test-org".to_string(),
169    ///     analysis_date: "2025-11-15T00:00:00Z".to_string(),
170    ///     repositories_analyzed: 10,
171    ///     commits_analyzed: 100,
172    ///     analyzer_version: "0.1.0".to_string(),
173    /// };
174    ///
175    /// let report = AnalysisReport {
176    ///     version: "1.0".to_string(),
177    ///     metadata,
178    ///     defect_patterns: vec![],
179    /// };
180    ///
181    /// generator.write_to_file(&report, &PathBuf::from("report.yaml")).await?;
182    /// # Ok(())
183    /// # }
184    /// ```
185    pub async fn write_to_file(&self, report: &AnalysisReport, path: &Path) -> Result<()> {
186        info!("Writing report to file: {}", path.display());
187
188        // Serialize to YAML
189        let yaml = self.to_yaml(report)?;
190
191        // Write to file
192        fs::write(path, yaml).await?;
193
194        info!("Successfully wrote report to {}", path.display());
195        Ok(())
196    }
197}
198
199impl Default for ReportGenerator {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_report_generator_creation() {
211        let _generator = ReportGenerator::new();
212        let _generator_default = ReportGenerator;
213    }
214
215    #[test]
216    fn test_yaml_serialization() {
217        let metadata = AnalysisMetadata {
218            organization: "test-org".to_string(),
219            analysis_date: "2025-11-15T00:00:00Z".to_string(),
220            repositories_analyzed: 5,
221            commits_analyzed: 50,
222            analyzer_version: "0.1.0".to_string(),
223        };
224
225        let report = AnalysisReport {
226            version: "1.0".to_string(),
227            metadata,
228            defect_patterns: vec![],
229        };
230
231        let generator = ReportGenerator::new();
232        let yaml = generator.to_yaml(&report).expect("Should serialize");
233
234        assert!(yaml.contains("version: '1.0'"));
235        assert!(yaml.contains("organization: test-org"));
236    }
237
238    #[test]
239    fn test_yaml_with_defect_patterns() {
240        let metadata = AnalysisMetadata {
241            organization: "test-org".to_string(),
242            analysis_date: "2025-11-15T00:00:00Z".to_string(),
243            repositories_analyzed: 10,
244            commits_analyzed: 100,
245            analyzer_version: "0.1.0".to_string(),
246        };
247
248        let patterns = vec![
249            DefectPattern {
250                category: DefectCategory::MemorySafety,
251                frequency: 42,
252                confidence: 0.85,
253                quality_signals: QualitySignals {
254                    avg_lines_changed: 45.2,
255                    avg_files_per_commit: 2.1,
256                    ..Default::default()
257                },
258                examples: vec![DefectInstance {
259                    commit_hash: "abc123".to_string(),
260                    message: "fix memory leak".to_string(),
261                    author: "test@example.com".to_string(),
262                    timestamp: 1234567890,
263                    files_affected: 2,
264                    lines_added: 30,
265                    lines_removed: 15,
266                }],
267            },
268            DefectPattern {
269                category: DefectCategory::ConcurrencyBugs,
270                frequency: 30,
271                confidence: 0.80,
272                quality_signals: QualitySignals {
273                    avg_lines_changed: 67.3,
274                    avg_files_per_commit: 3.5,
275                    ..Default::default()
276                },
277                examples: vec![DefectInstance {
278                    commit_hash: "def456".to_string(),
279                    message: "fix race condition".to_string(),
280                    author: "test@example.com".to_string(),
281                    timestamp: 1234567891,
282                    files_affected: 4,
283                    lines_added: 50,
284                    lines_removed: 17,
285                }],
286            },
287        ];
288
289        let report = AnalysisReport {
290            version: "1.0".to_string(),
291            metadata,
292            defect_patterns: patterns,
293        };
294
295        let generator = ReportGenerator::new();
296        let yaml = generator.to_yaml(&report).expect("Should serialize");
297
298        assert!(yaml.contains("MemorySafety"));
299        assert!(yaml.contains("ConcurrencyBugs"));
300        assert!(yaml.contains("frequency: 42"));
301    }
302
303    #[tokio::test]
304    async fn test_write_to_file() {
305        use tempfile::TempDir;
306
307        let temp_dir = TempDir::new().unwrap();
308        let report_path = temp_dir.path().join("test-report.yaml");
309
310        let metadata = AnalysisMetadata {
311            organization: "test-org".to_string(),
312            analysis_date: "2025-11-15T00:00:00Z".to_string(),
313            repositories_analyzed: 5,
314            commits_analyzed: 50,
315            analyzer_version: "0.1.0".to_string(),
316        };
317
318        let report = AnalysisReport {
319            version: "1.0".to_string(),
320            metadata,
321            defect_patterns: vec![],
322        };
323
324        let generator = ReportGenerator::new();
325        generator
326            .write_to_file(&report, &report_path)
327            .await
328            .expect("Should write file");
329
330        assert!(report_path.exists());
331
332        let content = tokio::fs::read_to_string(&report_path).await.unwrap();
333        assert!(content.contains("test-org"));
334    }
335
336    #[test]
337    fn test_quality_signals_default() {
338        let signals = QualitySignals::default();
339        assert!(signals.avg_tdg_score.is_none());
340        assert!(signals.avg_complexity.is_none());
341        assert_eq!(signals.satd_instances, 0);
342        assert_eq!(signals.avg_lines_changed, 0.0);
343    }
344
345    #[test]
346    fn test_quality_signals_with_values() {
347        let signals = QualitySignals {
348            avg_tdg_score: Some(2.5),
349            max_tdg_score: Some(5.0),
350            avg_complexity: Some(8.3),
351            avg_test_coverage: Some(0.75),
352            satd_instances: 10,
353            avg_lines_changed: 50.5,
354            avg_files_per_commit: 3.2,
355        };
356
357        assert_eq!(signals.avg_tdg_score, Some(2.5));
358        assert_eq!(signals.max_tdg_score, Some(5.0));
359        assert_eq!(signals.satd_instances, 10);
360    }
361
362    #[test]
363    fn test_defect_instance_structure() {
364        let instance = DefectInstance {
365            commit_hash: "abc123".to_string(),
366            message: "fix bug".to_string(),
367            author: "dev@example.com".to_string(),
368            timestamp: 1234567890,
369            files_affected: 3,
370            lines_added: 25,
371            lines_removed: 10,
372        };
373
374        assert_eq!(instance.commit_hash, "abc123");
375        assert_eq!(instance.files_affected, 3);
376        assert_eq!(instance.lines_added, 25);
377    }
378
379    #[test]
380    fn test_defect_pattern_structure() {
381        let pattern = DefectPattern {
382            category: DefectCategory::LogicErrors,
383            frequency: 15,
384            confidence: 0.70,
385            quality_signals: QualitySignals::default(),
386            examples: vec![],
387        };
388
389        assert_eq!(pattern.frequency, 15);
390        assert_eq!(pattern.confidence, 0.70);
391        assert!(pattern.examples.is_empty());
392    }
393
394    #[test]
395    fn test_analysis_metadata_structure() {
396        let metadata = AnalysisMetadata {
397            organization: "my-org".to_string(),
398            analysis_date: "2025-11-24T12:00:00Z".to_string(),
399            repositories_analyzed: 20,
400            commits_analyzed: 500,
401            analyzer_version: "0.2.0".to_string(),
402        };
403
404        assert_eq!(metadata.organization, "my-org");
405        assert_eq!(metadata.repositories_analyzed, 20);
406        assert_eq!(metadata.commits_analyzed, 500);
407    }
408
409    #[test]
410    fn test_report_serialization_deserialization() {
411        let metadata = AnalysisMetadata {
412            organization: "test".to_string(),
413            analysis_date: "2025-01-01T00:00:00Z".to_string(),
414            repositories_analyzed: 1,
415            commits_analyzed: 10,
416            analyzer_version: "0.1.0".to_string(),
417        };
418
419        let report = AnalysisReport {
420            version: "1.0".to_string(),
421            metadata,
422            defect_patterns: vec![],
423        };
424
425        let json = serde_json::to_string(&report).unwrap();
426        let deserialized: AnalysisReport = serde_json::from_str(&json).unwrap();
427
428        assert_eq!(report.version, deserialized.version);
429        assert_eq!(
430            report.metadata.organization,
431            deserialized.metadata.organization
432        );
433    }
434
435    #[test]
436    fn test_report_generator_default() {
437        let generator = ReportGenerator;
438        let metadata = AnalysisMetadata {
439            organization: "test".to_string(),
440            analysis_date: "2025-01-01T00:00:00Z".to_string(),
441            repositories_analyzed: 1,
442            commits_analyzed: 1,
443            analyzer_version: "0.1.0".to_string(),
444        };
445
446        let report = AnalysisReport {
447            version: "1.0".to_string(),
448            metadata,
449            defect_patterns: vec![],
450        };
451
452        let yaml = generator.to_yaml(&report).expect("Should serialize");
453        assert!(yaml.contains("version"));
454    }
455
456    #[test]
457    fn test_empty_defect_patterns() {
458        let metadata = AnalysisMetadata {
459            organization: "empty-org".to_string(),
460            analysis_date: "2025-01-01T00:00:00Z".to_string(),
461            repositories_analyzed: 0,
462            commits_analyzed: 0,
463            analyzer_version: "0.1.0".to_string(),
464        };
465
466        let report = AnalysisReport {
467            version: "1.0".to_string(),
468            metadata,
469            defect_patterns: vec![],
470        };
471
472        let generator = ReportGenerator::new();
473        let yaml = generator.to_yaml(&report).expect("Should serialize");
474
475        assert!(yaml.contains("defect_patterns: []"));
476    }
477}