use crate::summarizer::Summary;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PrWarning {
pub file: String,
pub category: String,
pub message: String,
pub prevention_tips: Vec<String>,
pub frequency: usize,
pub avg_tdg_score: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrReview {
pub warnings: Vec<PrWarning>,
pub baseline_date: String,
pub repositories_analyzed: usize,
pub files_analyzed: Vec<String>,
}
impl PrReview {
pub fn to_markdown(&self) -> String {
let mut output = String::new();
output.push_str("# PR Review: Organizational Intelligence\n\n");
if self.warnings.is_empty() {
output.push_str("✅ **No warnings** - No files match historical defect patterns.\n\n");
} else {
output.push_str(&format!(
"## ⚠️ {} Warning{} Based on Historical Patterns\n\n",
self.warnings.len(),
if self.warnings.len() == 1 { "" } else { "s" }
));
for warning in &self.warnings {
output.push_str(&format!("### {}\n", warning.file));
output.push_str(&format!(
"**Category**: {} ({} occurrences, TDG: {:.1})\n\n",
warning.category, warning.frequency, warning.avg_tdg_score
));
output.push_str(&format!("{}\n\n", warning.message));
if !warning.prevention_tips.is_empty() {
output.push_str("**Prevention Strategies**:\n");
for tip in &warning.prevention_tips {
output.push_str(&format!("- ✅ {}\n", tip));
}
output.push('\n');
}
output.push_str("---\n\n");
}
}
output.push_str(&format!(
"**Analysis Date**: {} (baseline is from {} repositories)\n",
self.baseline_date, self.repositories_analyzed
));
output.push_str(&format!(
"**Files Analyzed**: {}\n",
self.files_analyzed.len()
));
output
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string_pretty(self).context("Failed to serialize PR review to JSON")
}
}
pub struct PrReviewer {
baseline: Summary,
}
impl PrReviewer {
pub fn load_baseline<P: AsRef<Path>>(path: P) -> Result<Self> {
let baseline =
Summary::from_file(path.as_ref()).context("Failed to load baseline summary")?;
Ok(Self { baseline })
}
pub fn review_pr(&self, files_changed: &[String]) -> PrReview {
let mut warnings = Vec::new();
for file in files_changed {
let file_warnings = self.check_file_patterns(file);
warnings.extend(file_warnings);
}
PrReview {
warnings,
baseline_date: self.baseline.metadata.analysis_date.clone(),
repositories_analyzed: self.baseline.metadata.repositories_analyzed,
files_analyzed: files_changed.to_vec(),
}
}
fn check_file_patterns(&self, file: &str) -> Vec<PrWarning> {
let mut warnings = Vec::new();
if is_config_file(file) {
if let Some(pattern) = self.baseline.find_category("ConfigurationErrors") {
if should_warn(&pattern) {
warnings.push(PrWarning {
file: file.to_string(),
category: pattern.category.clone(),
message: format!(
"This org has {} config errors (avg TDG: {:.1}). Ensure validation!",
pattern.frequency, pattern.avg_tdg_score
),
prevention_tips: pattern.prevention_strategies.clone(),
frequency: pattern.frequency,
avg_tdg_score: pattern.avg_tdg_score,
});
}
}
}
if is_integration_file(file) {
if let Some(pattern) = self.baseline.find_category("IntegrationFailures") {
if should_warn(&pattern) {
warnings.push(PrWarning {
file: file.to_string(),
category: pattern.category.clone(),
message: format!(
"Integration issues detected {} times (avg TDG: {:.1}). Check timeouts and retries!",
pattern.frequency, pattern.avg_tdg_score
),
prevention_tips: pattern.prevention_strategies.clone(),
frequency: pattern.frequency,
avg_tdg_score: pattern.avg_tdg_score,
});
}
}
}
if is_code_file(file) {
if let Some(pattern) = self.baseline.find_category("LogicErrors") {
if should_warn(&pattern) {
warnings.push(PrWarning {
file: file.to_string(),
category: pattern.category.clone(),
message: format!(
"Logic errors occurred {} times (avg TDG: {:.1}). Test edge cases!",
pattern.frequency, pattern.avg_tdg_score
),
prevention_tips: pattern.prevention_strategies.clone(),
frequency: pattern.frequency,
avg_tdg_score: pattern.avg_tdg_score,
});
}
}
}
warnings
}
}
fn is_config_file(file: &str) -> bool {
file.ends_with(".yaml")
|| file.ends_with(".yml")
|| file.ends_with(".toml")
|| file.ends_with(".json")
|| file.ends_with(".env")
|| file.ends_with(".config")
|| file.ends_with(".ini")
}
fn is_integration_file(file: &str) -> bool {
let lower = file.to_lowercase();
lower.contains("api")
|| lower.contains("client")
|| lower.contains("http")
|| lower.contains("integration")
|| lower.contains("service")
}
fn is_code_file(file: &str) -> bool {
file.ends_with(".rs")
|| file.ends_with(".py")
|| file.ends_with(".js")
|| file.ends_with(".ts")
|| file.ends_with(".java")
|| file.ends_with(".go")
|| file.ends_with(".cpp")
|| file.ends_with(".c")
|| file.ends_with(".rb")
|| file.ends_with(".php")
}
fn should_warn(pattern: &crate::summarizer::DefectPatternSummary) -> bool {
(pattern.frequency >= 10 && pattern.avg_tdg_score < 60.0) || pattern.frequency >= 20
}
#[cfg(test)]
mod tests {
use super::*;
use crate::summarizer::{QualityThresholds, SummaryMetadata};
fn create_test_summary() -> Summary {
use crate::classifier::DefectCategory;
use crate::report::{DefectPattern, QualitySignals};
use crate::summarizer::OrganizationalInsights;
Summary {
organizational_insights: OrganizationalInsights {
top_defect_categories: vec![
DefectPattern {
category: DefectCategory::ConfigurationErrors,
frequency: 25,
confidence: 0.78,
quality_signals: QualitySignals {
avg_tdg_score: Some(45.2),
max_tdg_score: Some(60.0),
avg_complexity: Some(8.0),
avg_test_coverage: Some(0.5),
satd_instances: 5,
avg_lines_changed: 10.0,
avg_files_per_commit: 2.0,
},
examples: vec![],
},
DefectPattern {
category: DefectCategory::IntegrationFailures,
frequency: 18,
confidence: 0.82,
quality_signals: QualitySignals {
avg_tdg_score: Some(52.3),
max_tdg_score: Some(70.0),
avg_complexity: Some(6.0),
avg_test_coverage: Some(0.6),
satd_instances: 3,
avg_lines_changed: 15.0,
avg_files_per_commit: 3.0,
},
examples: vec![],
},
DefectPattern {
category: DefectCategory::LogicErrors,
frequency: 5, confidence: 0.91,
quality_signals: QualitySignals {
avg_tdg_score: Some(88.5),
max_tdg_score: Some(95.0),
avg_complexity: Some(4.0),
avg_test_coverage: Some(0.85),
satd_instances: 0,
avg_lines_changed: 8.0,
avg_files_per_commit: 1.5,
},
examples: vec![],
},
],
},
code_quality_thresholds: QualityThresholds::default(),
metadata: SummaryMetadata {
analysis_date: "2025-11-15".to_string(),
repositories_analyzed: 25,
commits_analyzed: 2500,
},
}
}
#[test]
fn test_config_file_triggers_warning() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&["config.yaml".to_string()]);
assert_eq!(review.warnings.len(), 1);
assert_eq!(review.warnings[0].category, "ConfigurationErrors");
assert_eq!(review.warnings[0].file, "config.yaml");
assert_eq!(review.warnings[0].frequency, 25);
}
#[test]
fn test_integration_file_triggers_warning() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&["src/api_client.rs".to_string()]);
assert_eq!(review.warnings.len(), 1);
let integration_warning = &review.warnings[0];
assert_eq!(integration_warning.category, "IntegrationFailures");
assert_eq!(integration_warning.file, "src/api_client.rs");
}
#[test]
fn test_low_frequency_no_warning() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&["src/logic.rs".to_string()]);
let logic_warnings: Vec<_> = review
.warnings
.iter()
.filter(|w| w.category == "LogicErrors")
.collect();
assert_eq!(logic_warnings.len(), 0);
}
#[test]
fn test_no_warnings_for_unmatched_files() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&["README.md".to_string()]);
assert_eq!(review.warnings.len(), 0);
}
#[test]
fn test_multiple_files_multiple_warnings() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&[
"config.yaml".to_string(),
"src/api.rs".to_string(),
"README.md".to_string(),
]);
assert!(review.warnings.len() >= 2);
assert_eq!(review.files_analyzed.len(), 3);
}
#[test]
fn test_markdown_output_with_warnings() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&["config.toml".to_string()]);
let markdown = review.to_markdown();
assert!(markdown.contains("# PR Review"));
assert!(markdown.contains("ConfigurationErrors"));
assert!(markdown.contains("config.toml"));
assert!(markdown.contains("Warning"));
}
#[test]
fn test_markdown_output_no_warnings() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&["README.md".to_string()]);
let markdown = review.to_markdown();
assert!(markdown.contains("No warnings"));
}
#[test]
fn test_json_output() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&["config.yaml".to_string()]);
let json = review.to_json().expect("Should serialize to JSON");
assert!(json.contains("ConfigurationErrors"));
assert!(json.contains("warnings"));
}
#[test]
fn test_file_type_detection() {
assert!(is_config_file("app.yaml"));
assert!(is_config_file("config.toml"));
assert!(is_config_file("settings.json"));
assert!(!is_config_file("main.rs"));
assert!(is_integration_file("api_client.rs"));
assert!(is_integration_file("http_service.go"));
assert!(!is_integration_file("main.rs"));
assert!(is_code_file("main.rs"));
assert!(is_code_file("app.py"));
assert!(!is_code_file("README.md"));
}
#[test]
fn test_should_warn_high_frequency_low_tdg() {
use crate::summarizer::DefectPatternSummary;
let pattern = DefectPatternSummary {
category: "Test".to_string(),
frequency: 15, confidence: 0.8,
avg_tdg_score: 55.0, common_patterns: vec![],
prevention_strategies: vec![],
};
assert!(should_warn(&pattern));
}
#[test]
fn test_should_warn_very_high_frequency() {
use crate::summarizer::DefectPatternSummary;
let pattern = DefectPatternSummary {
category: "Test".to_string(),
frequency: 25, confidence: 0.8,
avg_tdg_score: 85.0, common_patterns: vec![],
prevention_strategies: vec![],
};
assert!(should_warn(&pattern));
}
#[test]
fn test_should_not_warn_low_frequency_high_tdg() {
use crate::summarizer::DefectPatternSummary;
let pattern = DefectPatternSummary {
category: "Test".to_string(),
frequency: 8, confidence: 0.8,
avg_tdg_score: 85.0,
common_patterns: vec![],
prevention_strategies: vec![],
};
assert!(!should_warn(&pattern));
}
#[test]
fn test_config_file_all_extensions() {
assert!(is_config_file("file.yaml"));
assert!(is_config_file("file.yml"));
assert!(is_config_file("file.toml"));
assert!(is_config_file("file.json"));
assert!(is_config_file("file.env"));
assert!(is_config_file("file.config"));
assert!(is_config_file("file.ini"));
assert!(!is_config_file("file.txt"));
}
#[test]
fn test_integration_file_all_patterns() {
assert!(is_integration_file("api_service.rs"));
assert!(is_integration_file("http_client.go"));
assert!(is_integration_file("rest_api.py"));
assert!(is_integration_file("integration_tests.rs"));
assert!(is_integration_file("user_service.js"));
assert!(is_integration_file("API_client.rs")); assert!(!is_integration_file("main.rs"));
}
#[test]
fn test_code_file_all_extensions() {
assert!(is_code_file("main.rs"));
assert!(is_code_file("script.py"));
assert!(is_code_file("app.js"));
assert!(is_code_file("component.ts"));
assert!(is_code_file("App.java"));
assert!(is_code_file("server.go"));
assert!(is_code_file("utils.cpp"));
assert!(is_code_file("lib.c"));
assert!(is_code_file("controller.rb"));
assert!(is_code_file("index.php"));
assert!(!is_code_file("README.md"));
assert!(!is_code_file("config.yaml"));
}
#[test]
fn test_pr_warning_equality() {
let warning1 = PrWarning {
file: "test.rs".to_string(),
category: "Test".to_string(),
message: "Test message".to_string(),
prevention_tips: vec!["Tip 1".to_string()],
frequency: 10,
avg_tdg_score: 50.0,
};
let warning2 = warning1.clone();
assert_eq!(warning1, warning2);
}
#[test]
fn test_markdown_plural_logic() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review1 = reviewer.review_pr(&["config.yaml".to_string()]);
let markdown1 = review1.to_markdown();
assert!(markdown1.contains("1 Warning"));
let review2 =
reviewer.review_pr(&["config.yaml".to_string(), "src/api_client.rs".to_string()]);
let markdown2 = review2.to_markdown();
assert!(markdown2.contains("Warnings"));
}
#[test]
fn test_load_baseline_invalid_path() {
let result = PrReviewer::load_baseline("nonexistent.yaml");
assert!(result.is_err());
}
#[test]
fn test_review_empty_files() {
let summary = create_test_summary();
let reviewer = PrReviewer { baseline: summary };
let review = reviewer.review_pr(&[]);
assert_eq!(review.warnings.len(), 0);
assert_eq!(review.files_analyzed.len(), 0);
}
#[test]
fn test_markdown_with_prevention_tips() {
use crate::classifier::DefectCategory;
use crate::report::{DefectPattern, QualitySignals};
use crate::summarizer::OrganizationalInsights;
let summary = Summary {
organizational_insights: OrganizationalInsights {
top_defect_categories: vec![DefectPattern {
category: DefectCategory::ConfigurationErrors,
frequency: 25,
confidence: 0.78,
quality_signals: QualitySignals {
avg_tdg_score: Some(45.2),
max_tdg_score: Some(60.0),
avg_complexity: Some(8.0),
avg_test_coverage: Some(0.5),
satd_instances: 5,
avg_lines_changed: 10.0,
avg_files_per_commit: 2.0,
},
examples: vec![],
}],
},
code_quality_thresholds: QualityThresholds::default(),
metadata: SummaryMetadata {
analysis_date: "2025-11-15".to_string(),
repositories_analyzed: 25,
commits_analyzed: 2500,
},
};
let reviewer = PrReviewer { baseline: summary };
let mut review = reviewer.review_pr(&["config.yaml".to_string()]);
review.warnings[0].prevention_tips = vec!["Use validation".to_string()];
let markdown = review.to_markdown();
assert!(markdown.contains("Prevention Strategies"));
assert!(markdown.contains("Use validation"));
}
}