use super::checks::{CheckResult, CheckStatus};
use super::conventions::DeviationKind;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Finding {
pub convention: String,
pub severity: Severity,
pub file: String,
pub description: String,
pub suggestion: String,
pub kind: DeviationKind,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Warning,
Info,
}
pub fn build_findings(results: &[CheckResult]) -> Vec<Finding> {
let mut findings = Vec::new();
for result in results {
let severity = match result.status {
CheckStatus::Clean => continue,
CheckStatus::Drift => Severity::Warning,
CheckStatus::Fragmented => continue,
};
for outlier in &result.outliers {
for deviation in &outlier.deviations {
let severity =
if outlier.noisy || matches!(deviation.kind, DeviationKind::NamingMismatch) {
Severity::Info
} else {
severity.clone()
};
findings.push(Finding {
convention: result.convention_name.clone(),
severity,
file: outlier.file.clone(),
description: deviation.description.clone(),
suggestion: deviation.suggestion.clone(),
kind: deviation.kind.clone(),
});
}
}
}
findings
}
#[cfg(test)]
mod tests {
use super::*;
use crate::code_audit::checks::CheckResult;
use crate::code_audit::conventions::{Deviation, Outlier};
#[test]
fn clean_result_produces_no_findings() {
let results = vec![CheckResult {
convention_name: "Test".to_string(),
status: CheckStatus::Clean,
conforming_count: 3,
total_count: 3,
outliers: vec![],
}];
let findings = build_findings(&results);
assert!(findings.is_empty());
}
#[test]
fn drift_produces_warning_findings() {
let results = vec![CheckResult {
convention_name: "Step Types".to_string(),
status: CheckStatus::Drift,
conforming_count: 2,
total_count: 3,
outliers: vec![Outlier {
file: "agent-ping.php".to_string(),
noisy: false,
deviations: vec![Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing method: validate".to_string(),
suggestion: "Add validate()".to_string(),
}],
}],
}];
let findings = build_findings(&results);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].severity, Severity::Warning);
assert_eq!(findings[0].convention, "Step Types");
assert_eq!(findings[0].file, "agent-ping.php");
}
#[test]
fn fragmented_produces_no_findings() {
let results = vec![CheckResult {
convention_name: "Misc".to_string(),
status: CheckStatus::Fragmented,
conforming_count: 1,
total_count: 3,
outliers: vec![
Outlier {
file: "a.php".to_string(),
noisy: false,
deviations: vec![Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing".to_string(),
suggestion: "Fix".to_string(),
}],
},
Outlier {
file: "b.php".to_string(),
noisy: false,
deviations: vec![Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing".to_string(),
suggestion: "Fix".to_string(),
}],
},
],
}];
let findings = build_findings(&results);
assert!(
findings.is_empty(),
"Fragmented conventions should not produce findings"
);
}
#[test]
fn naming_mismatch_is_downgraded_to_info() {
let results = vec![CheckResult {
convention_name: "Abilities".to_string(),
status: CheckStatus::Drift,
conforming_count: 2,
total_count: 3,
outliers: vec![Outlier {
file: "abilities/helpers.php".to_string(),
noisy: true,
deviations: vec![Deviation {
kind: DeviationKind::NamingMismatch,
description:
"Helper-like name does not match convention suffix 'Ability': Helpers"
.to_string(),
suggestion: "Treat this as a utility/helper or rename it".to_string(),
}],
}],
}];
let findings = build_findings(&results);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].severity, Severity::Info);
assert_eq!(findings[0].kind, DeviationKind::NamingMismatch);
}
}