use parlov_core::SignalKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum SignalFamily {
Range,
CacheValidator,
Auth,
Precondition,
Negotiation,
ErrorBody,
General,
}
const FAMILY_CAP: u8 = 75;
pub(crate) struct SignalContribution {
pub family: SignalFamily,
pub confidence: f32,
pub impact: u8,
pub description: String,
}
pub(crate) struct FamilyAdjustedScores {
pub confidence_total: f32,
pub impact_total: u8,
pub family_count: usize,
}
pub(crate) fn apply_family_adjustment(
contributions: &[SignalContribution],
) -> FamilyAdjustedScores {
let mut family_counts = std::collections::HashMap::new();
let mut confidence_total: f32 = 0.0;
let mut impact_total: u16 = 0;
for contrib in contributions {
let count = family_counts
.entry(contrib.family)
.or_insert(0u8);
let capped = family_adjusted_confidence(contrib.confidence, *count);
confidence_total += capped;
impact_total += u16::from(contrib.impact);
*count += 1;
}
FamilyAdjustedScores {
confidence_total,
impact_total: impact_total.min(255) as u8,
family_count: family_counts.len(),
}
}
fn family_adjusted_confidence(raw: f32, position: u8) -> f32 {
let cap = f32::from(FAMILY_CAP);
let clamped = raw.min(cap);
match position {
0 => clamped,
1 => clamped * 0.5,
_ => 0.0,
}
}
pub(crate) fn header_family(name: &str) -> SignalFamily {
match name {
"content-range" | "accept-ranges" => SignalFamily::Range,
"etag" | "last-modified" => SignalFamily::CacheValidator,
"www-authenticate" => SignalFamily::Auth,
_ => SignalFamily::General,
}
}
pub(crate) fn status_code_family(baseline_status: u16) -> SignalFamily {
match baseline_status {
206 | 416 => SignalFamily::Range,
304 => SignalFamily::CacheValidator,
401 | 403 => SignalFamily::Auth,
412 => SignalFamily::Precondition,
406 => SignalFamily::Negotiation,
_ => SignalFamily::General,
}
}
pub(crate) fn corroboration_bonus(family_count: usize) -> u8 {
match family_count {
0 | 1 => 0,
2 => 3,
3 => 6,
_ => 8,
}
}
pub(crate) fn signal_kind_family(_kind: SignalKind) -> SignalFamily {
SignalFamily::General
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_signal_gets_full_points() {
let contributions = vec![SignalContribution {
family: SignalFamily::Range,
confidence: 12.0,
impact: 8,
description: "test".into(),
}];
let result = apply_family_adjustment(&contributions);
assert!((result.confidence_total - 12.0).abs() < 0.01);
assert_eq!(result.impact_total, 8);
}
#[test]
fn second_signal_in_family_gets_half() {
let contributions = vec![
SignalContribution {
family: SignalFamily::Range,
confidence: 12.0,
impact: 8,
description: "first".into(),
},
SignalContribution {
family: SignalFamily::Range,
confidence: 10.0,
impact: 5,
description: "second".into(),
},
];
let result = apply_family_adjustment(&contributions);
assert!((result.confidence_total - 17.0).abs() < 0.01);
assert_eq!(result.impact_total, 13);
}
#[test]
fn third_signal_in_family_gets_zero_confidence() {
let contributions = vec![
SignalContribution {
family: SignalFamily::Range,
confidence: 12.0,
impact: 8,
description: "first".into(),
},
SignalContribution {
family: SignalFamily::Range,
confidence: 10.0,
impact: 5,
description: "second".into(),
},
SignalContribution {
family: SignalFamily::Range,
confidence: 8.0,
impact: 15,
description: "third".into(),
},
];
let result = apply_family_adjustment(&contributions);
assert!((result.confidence_total - 17.0).abs() < 0.01);
assert_eq!(result.impact_total, 28);
}
#[test]
fn different_families_count_independently() {
let contributions = vec![
SignalContribution {
family: SignalFamily::Range,
confidence: 12.0,
impact: 8,
description: "range".into(),
},
SignalContribution {
family: SignalFamily::Auth,
confidence: 8.0,
impact: 8,
description: "auth".into(),
},
];
let result = apply_family_adjustment(&contributions);
assert!((result.confidence_total - 20.0).abs() < 0.01);
assert_eq!(result.family_count, 2);
}
#[test]
fn corroboration_bonus_values() {
assert_eq!(corroboration_bonus(0), 0);
assert_eq!(corroboration_bonus(1), 0);
assert_eq!(corroboration_bonus(2), 3);
assert_eq!(corroboration_bonus(3), 6);
assert_eq!(corroboration_bonus(5), 8);
}
#[test]
fn header_family_mappings() {
assert_eq!(header_family("content-range"), SignalFamily::Range);
assert_eq!(header_family("etag"), SignalFamily::CacheValidator);
assert_eq!(header_family("www-authenticate"), SignalFamily::Auth);
assert_eq!(header_family("x-custom"), SignalFamily::General);
}
#[test]
fn error_body_family_is_distinct() {
assert_ne!(SignalFamily::ErrorBody, SignalFamily::Range);
assert_ne!(SignalFamily::ErrorBody, SignalFamily::CacheValidator);
assert_ne!(SignalFamily::ErrorBody, SignalFamily::Auth);
assert_ne!(SignalFamily::ErrorBody, SignalFamily::Precondition);
assert_ne!(SignalFamily::ErrorBody, SignalFamily::Negotiation);
assert_ne!(SignalFamily::ErrorBody, SignalFamily::General);
}
}