ricecoder_generation/
report_generator.rs

1//! Report generation for code generation results
2//!
3//! Produces generation reports with statistics including:
4//! - File count and lines generated
5//! - Validation results
6//! - Conflict statistics
7//! - Time elapsed and tokens used
8//!
9//! Implements Requirement 1.6: Generation report with statistics
10
11use crate::conflict_detector::FileConflictInfo;
12use crate::models::{GeneratedFile, ValidationResult};
13use crate::review_engine::ReviewResult;
14use serde::{Deserialize, Serialize};
15use std::time::Duration;
16
17/// Statistics about code generation
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct GenerationStats {
20    /// Number of tokens used in generation
21    pub tokens_used: usize,
22    /// Time elapsed during generation
23    pub time_elapsed: Duration,
24    /// Number of files generated
25    pub files_generated: usize,
26    /// Total lines of code generated
27    pub lines_generated: usize,
28    /// Number of conflicts detected
29    pub conflicts_detected: usize,
30    /// Number of conflicts resolved
31    pub conflicts_resolved: usize,
32}
33
34impl Default for GenerationStats {
35    fn default() -> Self {
36        Self {
37            tokens_used: 0,
38            time_elapsed: Duration::ZERO,
39            files_generated: 0,
40            lines_generated: 0,
41            conflicts_detected: 0,
42            conflicts_resolved: 0,
43        }
44    }
45}
46
47/// Complete result of code generation
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct GenerationResult {
50    /// Generated files
51    pub files: Vec<GeneratedFile>,
52    /// Validation results
53    pub validation: ValidationResult,
54    /// Review results (optional)
55    pub review: Option<ReviewResult>,
56    /// Detected conflicts
57    pub conflicts: Vec<FileConflictInfo>,
58    /// Generation statistics
59    pub stats: GenerationStats,
60}
61
62impl GenerationResult {
63    /// Create a new generation result
64    pub fn new(
65        files: Vec<GeneratedFile>,
66        validation: ValidationResult,
67        conflicts: Vec<FileConflictInfo>,
68        stats: GenerationStats,
69    ) -> Self {
70        Self {
71            files,
72            validation,
73            review: None,
74            conflicts,
75            stats,
76        }
77    }
78
79    /// Add review results to the generation result
80    pub fn with_review(mut self, review: ReviewResult) -> Self {
81        self.review = Some(review);
82        self
83    }
84}
85
86/// A generation report with formatted statistics
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct GenerationReport {
89    /// Report title
90    pub title: String,
91    /// Report timestamp (ISO 8601 format)
92    pub timestamp: String,
93    /// Summary of generation
94    pub summary: ReportSummary,
95    /// File statistics
96    pub file_stats: FileStatistics,
97    /// Validation report
98    pub validation_report: ValidationReport,
99    /// Conflict report
100    pub conflict_report: ConflictReport,
101    /// Review report (optional)
102    pub review_report: Option<ReviewReport>,
103    /// Performance metrics
104    pub performance: PerformanceMetrics,
105}
106
107/// Summary of generation
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ReportSummary {
110    /// Whether generation was successful
111    pub success: bool,
112    /// Overall status message
113    pub status: String,
114    /// Number of files generated
115    pub files_generated: usize,
116    /// Total lines of code generated
117    pub lines_generated: usize,
118}
119
120/// File statistics
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct FileStatistics {
123    /// Total files generated
124    pub total_files: usize,
125    /// Files by language
126    pub files_by_language: std::collections::HashMap<String, usize>,
127    /// Total lines of code
128    pub total_lines: usize,
129    /// Average lines per file
130    pub average_lines_per_file: f64,
131}
132
133/// Validation report
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ValidationReport {
136    /// Whether validation passed
137    pub passed: bool,
138    /// Number of errors found
139    pub error_count: usize,
140    /// Number of warnings found
141    pub warning_count: usize,
142    /// Errors by file
143    pub errors_by_file: std::collections::HashMap<String, usize>,
144    /// Warnings by file
145    pub warnings_by_file: std::collections::HashMap<String, usize>,
146}
147
148/// Conflict report
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ConflictReport {
151    /// Total conflicts detected
152    pub total_conflicts: usize,
153    /// Conflicts resolved
154    pub conflicts_resolved: usize,
155    /// Conflicts pending
156    pub conflicts_pending: usize,
157    /// Conflicts by strategy
158    pub conflicts_by_strategy: std::collections::HashMap<String, usize>,
159}
160
161/// Review report
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct ReviewReport {
164    /// Overall quality score (0-100)
165    pub quality_score: f64,
166    /// Number of suggestions
167    pub suggestion_count: usize,
168    /// Number of issues found
169    pub issue_count: usize,
170}
171
172/// Performance metrics
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct PerformanceMetrics {
175    /// Time elapsed in seconds
176    pub time_elapsed_seconds: f64,
177    /// Tokens used
178    pub tokens_used: usize,
179    /// Files per second
180    pub files_per_second: f64,
181    /// Lines per second
182    pub lines_per_second: f64,
183}
184
185/// Generates reports from generation results
186pub struct ReportGenerator;
187
188impl ReportGenerator {
189    /// Generate a report from generation results
190    pub fn generate(result: &GenerationResult) -> GenerationReport {
191        let timestamp = chrono::Local::now().to_rfc3339();
192
193        // Calculate file statistics
194        let file_stats = Self::calculate_file_stats(&result.files);
195
196        // Calculate validation report
197        let validation_report = Self::calculate_validation_report(&result.validation);
198
199        // Calculate conflict report
200        let conflict_report = Self::calculate_conflict_report(&result.conflicts);
201
202        // Calculate performance metrics
203        let performance = Self::calculate_performance(&result.stats);
204
205        // Calculate review report if available
206        let review_report = result.review.as_ref().map(|review| ReviewReport {
207            quality_score: (review.overall_score * 100.0) as f64,
208            suggestion_count: review.suggestions.len(),
209            issue_count: review.issues.len(),
210        });
211
212        // Determine overall success
213        let success = result.validation.valid && result.conflicts.is_empty();
214        let status = if success {
215            "Generation completed successfully".to_string()
216        } else if !result.validation.valid {
217            format!(
218                "Generation completed with {} validation errors",
219                result.validation.errors.len()
220            )
221        } else {
222            format!(
223                "Generation completed with {} conflicts detected",
224                result.conflicts.len()
225            )
226        };
227
228        GenerationReport {
229            title: "Code Generation Report".to_string(),
230            timestamp,
231            summary: ReportSummary {
232                success,
233                status,
234                files_generated: result.files.len(),
235                lines_generated: file_stats.total_lines,
236            },
237            file_stats,
238            validation_report,
239            conflict_report,
240            review_report,
241            performance,
242        }
243    }
244
245    /// Generate a report as formatted text
246    pub fn generate_text(result: &GenerationResult) -> String {
247        let report = Self::generate(result);
248        Self::format_report(&report)
249    }
250
251    /// Generate a report as JSON
252    pub fn generate_json(result: &GenerationResult) -> Result<String, serde_json::Error> {
253        let report = Self::generate(result);
254        serde_json::to_string_pretty(&report)
255    }
256
257    fn calculate_file_stats(files: &[GeneratedFile]) -> FileStatistics {
258        let mut files_by_language = std::collections::HashMap::new();
259        let mut total_lines = 0;
260
261        for file in files {
262            let line_count = file.content.lines().count();
263            total_lines += line_count;
264
265            *files_by_language.entry(file.language.clone()).or_insert(0) += 1;
266        }
267
268        let average_lines_per_file = if files.is_empty() {
269            0.0
270        } else {
271            total_lines as f64 / files.len() as f64
272        };
273
274        FileStatistics {
275            total_files: files.len(),
276            files_by_language,
277            total_lines,
278            average_lines_per_file,
279        }
280    }
281
282    fn calculate_validation_report(validation: &ValidationResult) -> ValidationReport {
283        let mut errors_by_file = std::collections::HashMap::new();
284        let mut warnings_by_file = std::collections::HashMap::new();
285
286        for error in &validation.errors {
287            *errors_by_file.entry(error.file.clone()).or_insert(0) += 1;
288        }
289
290        for warning in &validation.warnings {
291            *warnings_by_file.entry(warning.file.clone()).or_insert(0) += 1;
292        }
293
294        ValidationReport {
295            passed: validation.valid,
296            error_count: validation.errors.len(),
297            warning_count: validation.warnings.len(),
298            errors_by_file,
299            warnings_by_file,
300        }
301    }
302
303    fn calculate_conflict_report(conflicts: &[FileConflictInfo]) -> ConflictReport {
304        let mut conflicts_by_strategy = std::collections::HashMap::new();
305
306        // All conflicts are initially pending (no strategy assigned yet)
307        if !conflicts.is_empty() {
308            conflicts_by_strategy.insert("Pending".to_string(), conflicts.len());
309        }
310
311        ConflictReport {
312            total_conflicts: conflicts.len(),
313            conflicts_resolved: 0, // Will be updated when conflicts are resolved
314            conflicts_pending: conflicts.len(),
315            conflicts_by_strategy,
316        }
317    }
318
319    fn calculate_performance(stats: &GenerationStats) -> PerformanceMetrics {
320        let time_elapsed_seconds = stats.time_elapsed.as_secs_f64();
321        let files_per_second = if time_elapsed_seconds > 0.0 {
322            stats.files_generated as f64 / time_elapsed_seconds
323        } else {
324            0.0
325        };
326        let lines_per_second = if time_elapsed_seconds > 0.0 {
327            stats.lines_generated as f64 / time_elapsed_seconds
328        } else {
329            0.0
330        };
331
332        PerformanceMetrics {
333            time_elapsed_seconds,
334            tokens_used: stats.tokens_used,
335            files_per_second,
336            lines_per_second,
337        }
338    }
339
340    fn format_report(report: &GenerationReport) -> String {
341        let mut output = String::new();
342
343        output.push_str("╔════════════════════════════════════════════════════════════╗\n");
344        output.push_str(&format!(
345            "║ {}                                    ║\n",
346            report.title
347        ));
348        output.push_str("╚════════════════════════════════════════════════════════════╝\n\n");
349
350        output.push_str(&format!("Timestamp: {}\n\n", report.timestamp));
351
352        // Summary
353        output.push_str("SUMMARY\n");
354        output.push_str("───────────────────────────────────────────────────────────────\n");
355        output.push_str(&format!("Status: {}\n", report.summary.status));
356        output.push_str(&format!(
357            "Files Generated: {}\n",
358            report.summary.files_generated
359        ));
360        output.push_str(&format!(
361            "Lines Generated: {}\n\n",
362            report.summary.lines_generated
363        ));
364
365        // File Statistics
366        output.push_str("FILE STATISTICS\n");
367        output.push_str("───────────────────────────────────────────────────────────────\n");
368        output.push_str(&format!("Total Files: {}\n", report.file_stats.total_files));
369        output.push_str(&format!("Total Lines: {}\n", report.file_stats.total_lines));
370        output.push_str(&format!(
371            "Average Lines per File: {:.2}\n",
372            report.file_stats.average_lines_per_file
373        ));
374
375        if !report.file_stats.files_by_language.is_empty() {
376            output.push_str("\nFiles by Language:\n");
377            for (lang, count) in &report.file_stats.files_by_language {
378                output.push_str(&format!("  {}: {}\n", lang, count));
379            }
380        }
381        output.push('\n');
382
383        // Validation Report
384        output.push_str("VALIDATION RESULTS\n");
385        output.push_str("───────────────────────────────────────────────────────────────\n");
386        output.push_str(&format!(
387            "Status: {}\n",
388            if report.validation_report.passed {
389                "PASSED"
390            } else {
391                "FAILED"
392            }
393        ));
394        output.push_str(&format!(
395            "Errors: {}\n",
396            report.validation_report.error_count
397        ));
398        output.push_str(&format!(
399            "Warnings: {}\n\n",
400            report.validation_report.warning_count
401        ));
402
403        // Conflict Report
404        output.push_str("CONFLICT DETECTION\n");
405        output.push_str("───────────────────────────────────────────────────────────────\n");
406        output.push_str(&format!(
407            "Total Conflicts: {}\n",
408            report.conflict_report.total_conflicts
409        ));
410        output.push_str(&format!(
411            "Conflicts Resolved: {}\n",
412            report.conflict_report.conflicts_resolved
413        ));
414        output.push_str(&format!(
415            "Conflicts Pending: {}\n\n",
416            report.conflict_report.conflicts_pending
417        ));
418
419        // Performance Metrics
420        output.push_str("PERFORMANCE METRICS\n");
421        output.push_str("───────────────────────────────────────────────────────────────\n");
422        output.push_str(&format!(
423            "Time Elapsed: {:.2}s\n",
424            report.performance.time_elapsed_seconds
425        ));
426        output.push_str(&format!(
427            "Tokens Used: {}\n",
428            report.performance.tokens_used
429        ));
430        output.push_str(&format!(
431            "Files per Second: {:.2}\n",
432            report.performance.files_per_second
433        ));
434        output.push_str(&format!(
435            "Lines per Second: {:.2}\n\n",
436            report.performance.lines_per_second
437        ));
438
439        // Review Report
440        if let Some(review) = &report.review_report {
441            output.push_str("CODE REVIEW\n");
442            output.push_str("───────────────────────────────────────────────────────────────\n");
443            output.push_str(&format!("Quality Score: {:.1}/100\n", review.quality_score));
444            output.push_str(&format!("Suggestions: {}\n", review.suggestion_count));
445            output.push_str(&format!("Issues: {}\n\n", review.issue_count));
446        }
447
448        output.push_str("═══════════════════════════════════════════════════════════════\n");
449
450        output
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_generation_stats_default() {
460        let stats = GenerationStats::default();
461        assert_eq!(stats.tokens_used, 0);
462        assert_eq!(stats.files_generated, 0);
463        assert_eq!(stats.lines_generated, 0);
464        assert_eq!(stats.conflicts_detected, 0);
465        assert_eq!(stats.conflicts_resolved, 0);
466    }
467
468    #[test]
469    fn test_generation_result_creation() {
470        let files = vec![GeneratedFile {
471            path: "test.rs".to_string(),
472            content: "fn main() {}".to_string(),
473            language: "rust".to_string(),
474        }];
475        let validation = ValidationResult::default();
476        let conflicts = vec![];
477        let stats = GenerationStats {
478            files_generated: 1,
479            lines_generated: 1,
480            ..Default::default()
481        };
482
483        let result = GenerationResult::new(files, validation, conflicts, stats);
484        assert_eq!(result.files.len(), 1);
485        assert!(result.validation.valid);
486        assert!(result.conflicts.is_empty());
487    }
488
489    #[test]
490    fn test_report_generation() {
491        let files = vec![
492            GeneratedFile {
493                path: "test1.rs".to_string(),
494                content: "fn main() {\n    println!(\"Hello\");\n}".to_string(),
495                language: "rust".to_string(),
496            },
497            GeneratedFile {
498                path: "test2.rs".to_string(),
499                content: "fn helper() {}".to_string(),
500                language: "rust".to_string(),
501            },
502        ];
503        let validation = ValidationResult::default();
504        let conflicts = vec![];
505        let stats = GenerationStats {
506            files_generated: 2,
507            lines_generated: 4,
508            tokens_used: 100,
509            time_elapsed: Duration::from_secs(1),
510            ..Default::default()
511        };
512
513        let result = GenerationResult::new(files, validation, conflicts, stats);
514        let report = ReportGenerator::generate(&result);
515
516        assert_eq!(report.summary.files_generated, 2);
517        assert_eq!(report.summary.lines_generated, 4);
518        assert_eq!(report.file_stats.total_files, 2);
519        assert_eq!(report.performance.tokens_used, 100);
520    }
521
522    #[test]
523    fn test_report_text_generation() {
524        let files = vec![GeneratedFile {
525            path: "test.rs".to_string(),
526            content: "fn main() {}".to_string(),
527            language: "rust".to_string(),
528        }];
529        let validation = ValidationResult::default();
530        let conflicts = vec![];
531        let stats = GenerationStats::default();
532
533        let result = GenerationResult::new(files, validation, conflicts, stats);
534        let text = ReportGenerator::generate_text(&result);
535
536        assert!(text.contains("Code Generation Report"));
537        assert!(text.contains("SUMMARY"));
538        assert!(text.contains("FILE STATISTICS"));
539        assert!(text.contains("VALIDATION RESULTS"));
540    }
541
542    #[test]
543    fn test_report_json_generation() {
544        let files = vec![GeneratedFile {
545            path: "test.rs".to_string(),
546            content: "fn main() {}".to_string(),
547            language: "rust".to_string(),
548        }];
549        let validation = ValidationResult::default();
550        let conflicts = vec![];
551        let stats = GenerationStats::default();
552
553        let result = GenerationResult::new(files, validation, conflicts, stats);
554        let json = ReportGenerator::generate_json(&result);
555
556        assert!(json.is_ok());
557        let json_str = json.unwrap();
558        assert!(json_str.contains("\"title\""));
559        assert!(json_str.contains("\"timestamp\""));
560    }
561}