1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! Convert check results into actionable findings for the report.
use super::checks::{CheckResult, CheckStatus};
use super::conventions::DeviationKind;
/// An actionable finding from the code audit.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Finding {
/// The convention this finding relates to.
pub convention: String,
/// Severity of the finding.
pub severity: Severity,
/// The file with the issue.
pub file: String,
/// Human-readable description.
pub description: String,
/// Suggested action.
pub suggestion: String,
/// The kind of deviation.
pub kind: DeviationKind,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
/// Convention violation — should be fixed.
Warning,
/// Pattern is unclear — needs investigation.
Info,
}
/// Build findings from check results.
///
/// Fragmented conventions (< 50% confidence) are suppressed — the convention
/// metadata still appears in the report, but individual findings are noise
/// when the pattern itself is uncertain.
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,
// Suppress individual findings from fragmented conventions.
// The convention is still reported; just the per-file findings are omitted.
CheckStatus::Fragmented => continue,
};
for outlier in &result.outliers {
for deviation in &outlier.deviations {
findings.push(Finding {
convention: result.convention_name.clone(),
severity: severity.clone(),
file: outlier.file.clone(),
description: deviation.description.clone(),
suggestion: deviation.suggestion.clone(),
kind: deviation.kind.clone(),
});
}
}
}
findings
}
// ============================================================================
// Tests
// ============================================================================
#[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(),
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() {
// Fragmented conventions (< 50% confidence) are suppressed.
// The convention metadata still appears in the report, but individual
// findings are noise when the pattern itself is uncertain.
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(),
deviations: vec![Deviation {
kind: DeviationKind::MissingMethod,
description: "Missing".to_string(),
suggestion: "Fix".to_string(),
}],
},
Outlier {
file: "b.php".to_string(),
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"
);
}
}