use crate::commands::scan::Diag;
use crate::evidence::{Confidence, Verdict};
use crate::patterns::Severity;
use crate::server::models::{BacklogStats, FindingSummary, HealthComponent, HealthScore};
const FILES_FLOOR: f64 = 100.0;
const FILES_CEILING: f64 = 50_000.0;
const QUALITY_DRAG_PER_FINDING: f64 = 0.05;
const QUALITY_DRAG_CAP: f64 = 15.0;
const TRIAGE_FLOOR: usize = 20;
const STALE_PENALTY_PER_FINDING: f64 = 2.0;
const STALE_PENALTY_CAP: f64 = 10.0;
#[derive(Debug, Clone, Copy)]
pub struct HealthInputs<'a> {
pub summary: &'a FindingSummary,
pub findings: &'a [Diag],
pub triage_coverage: f64,
pub new_since_last: usize,
pub fixed_since_last: usize,
pub reintroduced: usize,
pub repo_files: Option<u64>,
pub backlog: Option<&'a BacklogStats>,
pub has_history: bool,
pub blanket_suppression_rate: Option<f64>,
}
pub fn compute(inp: &HealthInputs<'_>) -> HealthScore {
let weighted = aggregate_findings(inp.findings);
let size_divisor = size_divisor(inp.repo_files);
let density_weight = weighted.raw_weight / size_divisor;
let base_score = density_to_base_score(density_weight);
let quality_drag = quality_drag(weighted.quality_count);
let base_after_drag = (base_score - quality_drag).clamp(0.0, 100.0);
let ceiling = high_total_ceiling(weighted.effective_high);
let floor = high_total_floor(weighted.effective_high);
let score_clamped = base_after_drag.clamp(floor, ceiling);
let components = build_components(inp, &weighted, base_after_drag, size_divisor);
let modifier_sum = components
.iter()
.filter(|c| c.label != "Severity pressure")
.map(signed_modifier_contribution)
.sum::<f64>();
let final_uncapped = (score_clamped + modifier_sum).clamp(0.0, 100.0);
let score = final_uncapped.min(ceiling).max(floor).round() as u8;
let grade = grade_for(score).to_string();
HealthScore {
score,
grade,
components,
}
}
#[derive(Debug, Default)]
struct WeightedAggregate {
raw_weight: f64,
quality_count: usize,
effective_high: usize,
raw_high: usize,
raw_medium: usize,
raw_low_security: usize,
confidence_rate: f64,
symex_coverage: f64,
}
fn aggregate_findings(findings: &[Diag]) -> WeightedAggregate {
let mut agg = WeightedAggregate::default();
let mut effective_high_sum = 0.0f64;
let mut conf_score_sum = 0.0f64;
let mut taint_total = 0usize;
let mut taint_with_verdict = 0usize;
for f in findings {
let is_quality = f.id.contains(".quality.") || f.id.starts_with("quality.");
if is_quality {
agg.quality_count += 1;
continue;
}
let severity = f.severity;
let conf_factor = confidence_factor(f.confidence);
let verdict_factor = verdict_factor(f);
let context_factor = context_factor(f);
let credibility = (conf_factor * verdict_factor * context_factor).clamp(0.0, 1.2);
let weight = severity_base(severity) * credibility;
agg.raw_weight += weight;
match severity {
Severity::High => {
agg.raw_high += 1;
effective_high_sum += credibility;
}
Severity::Medium => agg.raw_medium += 1,
Severity::Low => agg.raw_low_security += 1,
}
conf_score_sum += match f.confidence {
Some(Confidence::High) => 1.0,
Some(Confidence::Medium) => 0.5,
_ => 0.0,
};
if let Some(ev) = f.evidence.as_ref()
&& ev.symbolic.is_some()
{
taint_total += 1;
if !matches!(
ev.symbolic.as_ref().map(|s| s.verdict),
Some(Verdict::NotAttempted) | None
) {
taint_with_verdict += 1;
}
}
}
agg.effective_high = effective_high_sum.round() as usize;
agg.confidence_rate = if findings.is_empty() {
100.0
} else {
let security_total = (findings.len() - agg.quality_count).max(1);
(conf_score_sum / security_total as f64) * 100.0
};
agg.symex_coverage = if taint_total == 0 {
0.0
} else {
taint_with_verdict as f64 / taint_total as f64
};
agg
}
fn severity_base(s: Severity) -> f64 {
match s {
Severity::High => 10.0,
Severity::Medium => 3.0,
Severity::Low => 0.5,
}
}
fn confidence_factor(c: Option<Confidence>) -> f64 {
match c {
Some(Confidence::High) => 1.0,
Some(Confidence::Medium) => 0.6,
Some(Confidence::Low) => 0.3,
None => 0.5,
}
}
fn verdict_factor(f: &Diag) -> f64 {
let Some(ev) = f.evidence.as_ref() else {
return 1.0;
};
let Some(sv) = ev.symbolic.as_ref() else {
return 1.0;
};
match sv.verdict {
Verdict::Confirmed => 1.2,
Verdict::NotAttempted => 1.0,
Verdict::Inconclusive => 0.7,
Verdict::Infeasible => 0.1,
}
}
fn context_factor(f: &Diag) -> f64 {
if is_test_path(&f.path) {
return 0.3;
}
let Some(ev) = f.evidence.as_ref() else {
return 0.75; };
if ev.flow_steps.is_empty() {
return 0.75;
}
if ev.flow_steps.iter().any(|s| s.is_cross_file) || ev.uses_summary {
return 1.15;
}
1.0
}
fn is_test_path(path: &str) -> bool {
let p = path.to_ascii_lowercase();
p.contains("/test/")
|| p.contains("/tests/")
|| p.contains("/spec/")
|| p.contains("/__tests__/")
|| p.contains("/testdata/")
|| p.ends_with("_test.go")
|| p.ends_with("_spec.rb")
|| p.ends_with(".test.ts")
|| p.ends_with(".test.js")
|| p.ends_with(".spec.ts")
|| p.ends_with(".spec.js")
|| file_basename(&p)
.map(|b| b.starts_with("test_") && b.ends_with(".py"))
.unwrap_or(false)
}
fn file_basename(path: &str) -> Option<&str> {
path.rsplit('/').next()
}
fn size_divisor(repo_files: Option<u64>) -> f64 {
let f = match repo_files {
Some(n) => (n as f64).clamp(FILES_FLOOR, FILES_CEILING),
None => FILES_FLOOR,
};
(f / FILES_FLOOR).sqrt()
}
fn density_to_base_score(density_weight: f64) -> f64 {
if density_weight <= 0.0 {
return 100.0;
}
let raw = 100.0 - 22.0 * (1.0 + density_weight / 4.0).log10();
raw.clamp(0.0, 100.0)
}
fn quality_drag(quality_count: usize) -> f64 {
(quality_count as f64 * QUALITY_DRAG_PER_FINDING).min(QUALITY_DRAG_CAP)
}
fn high_total_ceiling(effective_high: usize) -> f64 {
match effective_high {
0 => 100.0,
1 => 85.0, 2 => 78.0, 3..=5 => 68.0, 6..=10 => 58.0,
_ => 45.0,
}
}
fn high_total_floor(effective_high: usize) -> f64 {
if effective_high == 0 { 70.0 } else { 0.0 }
}
fn stale_high_penalty(effective_high: usize, backlog: Option<&BacklogStats>) -> f64 {
let Some(b) = backlog else { return 0.0 };
if effective_high == 0 || b.stale_count == 0 {
return 0.0;
}
(b.stale_count as f64 * STALE_PENALTY_PER_FINDING).min(STALE_PENALTY_CAP)
}
fn build_components(
inp: &HealthInputs<'_>,
weighted: &WeightedAggregate,
base_after_drag: f64,
size_divisor: f64,
) -> Vec<HealthComponent> {
let total = inp.summary.total;
let sev_score = base_after_drag.round().clamp(0.0, 100.0) as u8;
let sev_detail = severity_detail(weighted, size_divisor, inp.repo_files, inp.backlog);
let conf_score = weighted.confidence_rate.round().clamp(0.0, 100.0) as u8;
let conf_detail = format!(
"High-confidence rate {:.0}% across {} security finding{}",
weighted.confidence_rate,
total - weighted.quality_count,
plural_s(total - weighted.quality_count)
);
let net = inp.fixed_since_last as i64 - inp.new_since_last as i64;
let trend_score = (50 + net * 5).clamp(0, 100) as u8;
let trend_weight = if inp.has_history { 0.20 } else { 0.0 };
let trend_detail = if inp.has_history {
format!(
"Net {} since last scan ({} fixed, {} new)",
net, inp.fixed_since_last, inp.new_since_last
)
} else {
"Not applicable: no prior scan to compare against (re-scan to populate)".into()
};
let triage_active = total >= TRIAGE_FLOOR;
let triage_score = (inp.triage_coverage * 100.0).round().clamp(0.0, 100.0) as u8;
let triage_weight = if triage_active { 0.20 } else { 0.0 };
let triage_detail = if triage_active {
format!(
"{:.0}% of findings have a triage state",
inp.triage_coverage * 100.0
)
} else {
format!(
"Not applicable: only {} finding{} (need ≥{} to evaluate)",
total,
plural_s(total),
TRIAGE_FLOOR
)
};
let stale_penalty = stale_high_penalty(weighted.effective_high, inp.backlog);
let reintro_penalty = (inp.reintroduced as f64 * 5.0).min(10.0);
let regression_score = (100.0 - reintro_penalty - stale_penalty)
.clamp(0.0, 100.0)
.round() as u8;
let regression_detail = match (inp.reintroduced, stale_penalty) {
(0, 0.0) => "No reintroduced or stale-HIGH findings".into(),
(0, p) => format!(
"{} stale finding{} affecting HIGH severity (−{:.0})",
inp.backlog.map(|b| b.stale_count).unwrap_or(0),
plural_s(inp.backlog.map(|b| b.stale_count).unwrap_or(0)),
p
),
(n, 0.0) => format!(
"{} previously-fixed finding{} reintroduced (−{:.0})",
n,
plural_s(n),
(n as f64 * 5.0).min(10.0)
),
(n, p) => format!(
"{} reintroduced (−{:.0}) + stale-HIGH penalty (−{:.0})",
n,
(n as f64 * 5.0).min(10.0),
p
),
};
vec![
HealthComponent {
label: "Severity pressure".into(),
score: sev_score,
weight: 1.0, detail: sev_detail,
},
HealthComponent {
label: "Confidence quality".into(),
score: conf_score,
weight: 0.0, detail: conf_detail,
},
HealthComponent {
label: "Trend".into(),
score: trend_score,
weight: trend_weight,
detail: trend_detail,
},
HealthComponent {
label: "Triage coverage".into(),
score: triage_score,
weight: triage_weight,
detail: triage_detail,
},
HealthComponent {
label: "Regression resistance".into(),
score: regression_score,
weight: 0.15,
detail: regression_detail,
},
]
}
fn signed_modifier_contribution(c: &HealthComponent) -> f64 {
if c.weight == 0.0 {
return 0.0;
}
match c.label.as_str() {
"Confidence quality" => {
0.0
}
"Trend" => {
let centred = (c.score as f64 - 50.0) / 50.0;
(centred * 3.0).clamp(-3.0, 3.0)
}
"Triage coverage" => {
if c.score >= 50 {
((c.score as f64 - 50.0) / 50.0 * 5.0).min(5.0)
} else {
-((50.0 - c.score as f64) / 50.0 * 3.0).min(3.0)
}
}
"Regression resistance" => {
((c.score as f64 - 100.0) * 0.15).clamp(-15.0, 0.0)
}
_ => 0.0,
}
}
fn severity_detail(
w: &WeightedAggregate,
size_divisor: f64,
repo_files: Option<u64>,
backlog: Option<&BacklogStats>,
) -> String {
let mut parts = Vec::new();
parts.push(format!("{:.0} weighted points", w.raw_weight));
parts.push(format!(
"{} High, {} Medium, {} Low",
w.raw_high, w.raw_medium, w.raw_low_security
));
if w.quality_count > 0 {
parts.push(format!("{} quality lints", w.quality_count));
}
if w.effective_high != w.raw_high {
parts.push(format!(
"effective HIGH={} (credibility-adjusted)",
w.effective_high
));
}
if let Some(f) = repo_files
&& (size_divisor - 1.0).abs() > 0.01
{
parts.push(format!("size factor 1/{:.2}× ({} files)", size_divisor, f));
}
let stale = stale_high_penalty(w.effective_high, backlog);
if stale > 0.0
&& let Some(b) = backlog
{
parts.push(format!("−{:.0} stale-HIGH ({} >30d)", stale, b.stale_count));
}
parts.join(" · ")
}
fn grade_for(score: u8) -> &'static str {
match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F",
}
}
fn plural_s(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::patterns::{FindingCategory, Severity};
fn diag(severity: Severity, id: &str, conf: Option<Confidence>) -> Diag {
Diag {
path: "src/lib.rs".into(),
line: 1,
col: 1,
severity,
id: id.into(),
category: FindingCategory::Security,
path_validated: false,
guard_kind: None,
message: None,
labels: Vec::new(),
confidence: conf,
evidence: None,
rank_score: None,
rank_reason: None,
suppressed: false,
suppression: None,
rollup: None,
finding_id: String::new(),
alternative_finding_ids: Vec::new(),
}
}
fn diag_in(path: &str, severity: Severity, conf: Option<Confidence>) -> Diag {
let mut d = diag(severity, "rs.taint.x", conf);
d.path = path.into();
d
}
fn summary_of(findings: &[Diag]) -> FindingSummary {
let mut s = FindingSummary {
total: findings.len(),
..Default::default()
};
for d in findings {
*s.by_severity
.entry(d.severity.as_db_str().to_string())
.or_insert(0) += 1;
}
s
}
fn first_scan<'a>(
summary: &'a FindingSummary,
findings: &'a [Diag],
triage: f64,
files: u64,
) -> HealthInputs<'a> {
HealthInputs {
summary,
findings,
triage_coverage: triage,
new_since_last: 0,
fixed_since_last: 0,
reintroduced: 0,
repo_files: Some(files),
backlog: None,
has_history: false,
blanket_suppression_rate: None,
}
}
#[allow(dead_code)]
fn with_history<'a>(
summary: &'a FindingSummary,
findings: &'a [Diag],
triage: f64,
files: u64,
) -> HealthInputs<'a> {
HealthInputs {
has_history: true,
..first_scan(summary, findings, triage, files)
}
}
#[allow(dead_code)]
fn sev_score(h: &HealthScore) -> u8 {
h.components
.iter()
.find(|c| c.label == "Severity pressure")
.unwrap()
.score
}
#[test]
fn clean_repo_first_scan_grades_a() {
let findings: Vec<Diag> = vec![];
let s = summary_of(&findings);
let h = compute(&first_scan(&s, &findings, 0.0, 100));
assert_eq!(h.grade, "A");
assert!(h.score >= 95, "clean first-scan ≥95, got {}", h.score);
}
#[test]
fn no_high_repo_never_grades_below_c() {
let mut findings: Vec<Diag> = (0..200)
.map(|_| diag(Severity::Medium, "rs.taint.foo", Some(Confidence::High)))
.collect();
findings.extend(
(0..2000).map(|_| diag(Severity::Low, "rs.quality.unwrap", Some(Confidence::High))),
);
let s = summary_of(&findings);
let h = compute(&first_scan(&s, &findings, 0.0, 200));
assert!(h.score >= 70, "0 HIGH must grade ≥C (70), got {}", h.score);
}
#[test]
fn quality_lints_alone_grade_at_least_b() {
let findings: Vec<Diag> = (0..1000)
.map(|_| diag(Severity::Low, "rs.quality.unwrap", Some(Confidence::High)))
.collect();
let s = summary_of(&findings);
let h = compute(&first_scan(&s, &findings, 0.0, 100));
assert!(h.score >= 80, "1000 quality lints → ≥B, got {}", h.score);
}
#[test]
fn one_high_caps_at_b() {
let findings = vec![diag(Severity::High, "rs.taint.x", Some(Confidence::High))];
let s = summary_of(&findings);
let h = compute(&first_scan(&s, &findings, 0.0, 100));
assert!(h.score <= 89, "1 HIGH must not grade A, got {}", h.score);
assert_ne!(h.grade, "A");
}
#[test]
fn many_confirmed_high_grades_f() {
let findings: Vec<Diag> = (0..8)
.map(|_| {
let mut d = diag(Severity::High, "rs.taint.x", Some(Confidence::High));
let ev = crate::evidence::Evidence {
symbolic: Some(crate::evidence::SymbolicVerdict {
verdict: crate::evidence::Verdict::Confirmed,
constraints_checked: 0,
paths_explored: 0,
witness: None,
interproc_call_chains: Vec::new(),
cutoff_notes: Vec::new(),
}),
..Default::default()
};
d.evidence = Some(ev);
d
})
.collect();
let s = summary_of(&findings);
let h = compute(&first_scan(&s, &findings, 0.0, 1000));
assert_eq!(h.grade, "F");
}
#[test]
fn low_credibility_high_does_not_count_as_full() {
let findings: Vec<Diag> = (0..5)
.map(|_| {
let mut d = diag(Severity::High, "rs.taint.x", Some(Confidence::Low));
d.evidence = None;
d
})
.collect();
let s = summary_of(&findings);
let h = compute(&first_scan(&s, &findings, 0.0, 100));
assert!(
h.score >= 60,
"low-credibility HIGHs shouldn't crater to F, got {}",
h.score
);
}
#[test]
fn test_path_findings_are_discounted() {
let in_test = vec![diag_in(
"src/feature/__tests__/handler.test.ts",
Severity::High,
Some(Confidence::High),
)];
let in_prod = vec![diag_in(
"src/feature/handler.ts",
Severity::High,
Some(Confidence::High),
)];
let st = summary_of(&in_test);
let sp = summary_of(&in_prod);
let h_test = compute(&first_scan(&st, &in_test, 0.0, 50));
let h_prod = compute(&first_scan(&sp, &in_prod, 0.0, 50));
assert!(
h_test.score > h_prod.score,
"test-path HIGH ({}) should grade better than prod HIGH ({})",
h_test.score,
h_prod.score
);
}
#[test]
fn density_dampens_for_large_repos_but_caps() {
let findings: Vec<Diag> = (0..3)
.map(|_| diag(Severity::Medium, "rs.taint.x", Some(Confidence::High)))
.collect();
let s = summary_of(&findings);
let small = compute(&first_scan(&s, &findings, 0.0, 100));
let mid = compute(&first_scan(&s, &findings, 0.0, 5000));
let big = compute(&first_scan(&s, &findings, 0.0, 50_000));
let huge = compute(&first_scan(&s, &findings, 0.0, 500_000));
assert!(
small.score <= mid.score,
"small {} mid {}",
small.score,
mid.score
);
assert!(
mid.score <= big.score,
"mid {} big {}",
mid.score,
big.score
);
assert!(
(big.score as i32 - huge.score as i32).abs() <= 1,
"size cap broken: big {} huge {}",
big.score,
huge.score
);
}
#[test]
fn triage_drops_when_total_under_floor() {
let findings: Vec<Diag> = (0..5)
.map(|_| diag(Severity::Low, "rs.x", Some(Confidence::High)))
.collect();
let s = summary_of(&findings);
let h = compute(&first_scan(&s, &findings, 0.0, 100));
let triage = h
.components
.iter()
.find(|c| c.label == "Triage coverage")
.unwrap();
assert_eq!(triage.weight, 0.0);
assert!(triage.detail.contains("Not applicable"));
}
#[test]
fn trend_drops_on_first_scan() {
let findings: Vec<Diag> = (0..30)
.map(|_| diag(Severity::Medium, "rs.x", Some(Confidence::High)))
.collect();
let s = summary_of(&findings);
let h = compute(&first_scan(&s, &findings, 0.5, 100));
let trend = h.components.iter().find(|c| c.label == "Trend").unwrap();
assert_eq!(trend.weight, 0.0);
assert!(trend.detail.contains("Not applicable"));
}
#[test]
fn stale_high_penalty_lowers_regression_component() {
let findings = vec![diag(Severity::High, "rs.taint.x", Some(Confidence::High))];
let s = summary_of(&findings);
let backlog_clean = BacklogStats {
oldest_open_days: Some(2),
median_age_days: Some(1),
stale_count: 0,
age_buckets: vec![],
};
let backlog_stale = BacklogStats {
oldest_open_days: Some(120),
median_age_days: Some(60),
stale_count: 3,
age_buckets: vec![],
};
let fresh_inputs = HealthInputs {
backlog: Some(&backlog_clean),
has_history: true,
..first_scan(&s, &findings, 0.0, 100)
};
let rotting_inputs = HealthInputs {
backlog: Some(&backlog_stale),
has_history: true,
..first_scan(&s, &findings, 0.0, 100)
};
let fresh = compute(&fresh_inputs);
let rotting = compute(&rotting_inputs);
let fresh_reg = fresh
.components
.iter()
.find(|c| c.label == "Regression resistance")
.unwrap()
.score;
let rot_reg = rotting
.components
.iter()
.find(|c| c.label == "Regression resistance")
.unwrap()
.score;
assert!(
rot_reg < fresh_reg,
"stale should lower regression score: fresh {} vs rotting {}",
fresh_reg,
rot_reg
);
}
#[test]
fn grade_thresholds() {
assert_eq!(grade_for(100), "A");
assert_eq!(grade_for(90), "A");
assert_eq!(grade_for(89), "B");
assert_eq!(grade_for(80), "B");
assert_eq!(grade_for(79), "C");
assert_eq!(grade_for(70), "C");
assert_eq!(grade_for(69), "D");
assert_eq!(grade_for(60), "D");
assert_eq!(grade_for(59), "F");
assert_eq!(grade_for(0), "F");
}
}