use tracing::debug;
use crate::models::{Effort, Finding, Verdict};
const LOW_CONFIDENCE_THRESHOLD: f32 = 0.65;
pub fn derive_verdict(model_proposed: Verdict, findings: &[Finding]) -> Verdict {
if model_proposed == Verdict::Unknown {
debug!("verdict=UNKNOWN from model — preserving (diff unassessable)");
return Verdict::Unknown;
}
let has_high = findings.iter().any(|f| f.effort == Effort::High);
let all_low_confidence = !findings.is_empty()
&& findings
.iter()
.all(|f| f.confidence <= LOW_CONFIDENCE_THRESHOLD);
if all_low_confidence && !has_high {
debug!(
model_verdict = %model_proposed,
"low-confidence override: all findings ≤0.65 confidence, no High-effort → APPROVE"
);
return Verdict::Approve;
}
let floor = severity_floor(findings);
let final_verdict = stricter_of(model_proposed.clone(), floor.clone());
debug!(
model_verdict = %model_proposed,
severity_floor = %floor,
final_verdict = %final_verdict,
"grade derivation: floor={floor}, model={model_proposed}, final={final_verdict}",
);
final_verdict
}
fn severity_floor(findings: &[Finding]) -> Verdict {
if findings.is_empty() {
return Verdict::Approve;
}
let has_high = findings.iter().any(|f| f.effort == Effort::High);
let medium_count = findings
.iter()
.filter(|f| f.effort == Effort::Medium)
.count();
if has_high {
return Verdict::Block;
}
if medium_count >= 2 {
return Verdict::RequestChanges;
}
if medium_count == 1 {
return Verdict::ApproveWithReservations;
}
Verdict::Approve
}
fn stricter_of(a: Verdict, b: Verdict) -> Verdict {
if verdict_ord(&b) > verdict_ord(&a) {
b
} else {
a
}
}
fn verdict_ord(v: &Verdict) -> u8 {
match v {
Verdict::Approve => 0,
Verdict::ApproveWithReservations => 1,
Verdict::RequestChanges => 2,
Verdict::Block => 3,
Verdict::Unknown => 4, }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::Finding;
fn finding(effort: Effort, confidence: f32) -> Finding {
Finding::new("src/lib.rs", "test", "desc", "", confidence, effort)
}
#[test]
fn grade_critical_high_effort_yields_block() {
let findings = vec![finding(Effort::High, 0.9)];
let verdict = derive_verdict(Verdict::ApproveWithReservations, &findings);
assert_eq!(
verdict,
Verdict::Block,
"High-effort finding must floor to BLOCK"
);
}
#[test]
fn grade_high_effort_beats_request_changes() {
let findings = vec![finding(Effort::High, 0.85)];
let verdict = derive_verdict(Verdict::RequestChanges, &findings);
assert_eq!(verdict, Verdict::Block);
}
#[test]
fn grade_two_medium_yields_request_changes() {
let findings = vec![finding(Effort::Medium, 0.8), finding(Effort::Medium, 0.75)];
let verdict = derive_verdict(Verdict::ApproveWithReservations, &findings);
assert_eq!(verdict, Verdict::RequestChanges);
}
#[test]
fn grade_three_medium_yields_request_changes() {
let findings = vec![
finding(Effort::Medium, 0.7),
finding(Effort::Medium, 0.7),
finding(Effort::Medium, 0.7),
];
let verdict = derive_verdict(Verdict::Approve, &findings);
assert_eq!(verdict, Verdict::RequestChanges);
}
#[test]
fn grade_one_medium_yields_approve_star() {
let findings = vec![finding(Effort::Medium, 0.75)];
let verdict = derive_verdict(Verdict::Approve, &findings);
assert_eq!(verdict, Verdict::ApproveWithReservations);
}
#[test]
fn grade_no_findings_yields_approve() {
let verdict = derive_verdict(Verdict::Approve, &[]);
assert_eq!(verdict, Verdict::Approve);
}
#[test]
fn grade_only_low_yields_approve() {
let findings = vec![finding(Effort::Low, 0.9), finding(Effort::Low, 0.7)];
let verdict = derive_verdict(Verdict::Approve, &findings);
assert_eq!(verdict, Verdict::Approve);
}
#[test]
fn grade_unknown_is_preserved() {
let findings = vec![finding(Effort::Low, 0.9)];
let verdict = derive_verdict(Verdict::Unknown, &findings);
assert_eq!(verdict, Verdict::Unknown, "UNKNOWN must be preserved");
}
#[test]
fn grade_unknown_preserved_with_no_findings() {
let verdict = derive_verdict(Verdict::Unknown, &[]);
assert_eq!(verdict, Verdict::Unknown);
}
#[test]
fn grade_floor_overrides_model_approve() {
let findings = vec![finding(Effort::High, 0.95)];
let verdict = derive_verdict(Verdict::Approve, &findings);
assert_eq!(
verdict,
Verdict::Block,
"severity floor must override model-proposed APPROVE"
);
}
#[test]
fn grade_model_block_kept_when_no_critical_finding() {
let findings = vec![finding(Effort::Medium, 0.9)];
let verdict = derive_verdict(Verdict::Block, &findings);
assert_eq!(
verdict,
Verdict::Block,
"model BLOCK must not be downgraded by floor"
);
}
#[test]
fn grade_model_request_changes_preserved_over_lower_floor() {
let findings = vec![finding(Effort::Low, 0.9)];
let verdict = derive_verdict(Verdict::RequestChanges, &findings);
assert_eq!(
verdict,
Verdict::RequestChanges,
"model REQUEST_CHANGES must not be downgraded to APPROVE"
);
}
#[test]
fn grade_low_confidence_all_medium_yields_approve() {
let findings = vec![finding(Effort::Medium, 0.6), finding(Effort::Medium, 0.55)];
let verdict = derive_verdict(Verdict::ApproveWithReservations, &findings);
assert_eq!(
verdict,
Verdict::Approve,
"all-low-confidence advisory batch must not fire APPROVE*"
);
}
#[test]
fn grade_confidence_at_threshold_collapses() {
let findings = vec![finding(Effort::Medium, 0.65)];
let verdict = derive_verdict(Verdict::ApproveWithReservations, &findings);
assert_eq!(
verdict,
Verdict::Approve,
"confidence at threshold must collapse"
);
}
#[test]
fn grade_high_confidence_medium_beats_low_confidence_check() {
let findings = vec![finding(Effort::Medium, 0.66)];
let verdict = derive_verdict(Verdict::Approve, &findings);
assert_eq!(
verdict,
Verdict::ApproveWithReservations,
"confidence above threshold must yield APPROVE*"
);
}
#[test]
fn grade_mixed_confidence_two_medium_not_collapsed() {
let findings = vec![finding(Effort::Medium, 0.8), finding(Effort::Medium, 0.5)];
let verdict = derive_verdict(Verdict::Approve, &findings);
assert_eq!(
verdict,
Verdict::RequestChanges,
"mixed-confidence Medium findings must not collapse"
);
}
#[test]
fn grade_compile_break_high_effort_flows_to_block() {
let findings = vec![finding(Effort::High, 0.95)];
let verdict = derive_verdict(Verdict::ApproveWithReservations, &findings);
assert_eq!(
verdict,
Verdict::Block,
"compile-break (High effort) must escalate to BLOCK"
);
}
}