#[derive(Debug, Clone, PartialEq)]
pub struct EvidenceInputs {
pub top_rerank_score: f64,
pub top_lexical_indicator: f64,
pub top_result_link_count: u32,
pub days_since_top_result_modified: f64,
}
#[must_use]
pub fn compute_evidence_score(inputs: &EvidenceInputs) -> f64 {
let rerank = inputs.top_rerank_score.clamp(0.0, 1.0);
let lexical = inputs.top_lexical_indicator.clamp(0.0, 1.0);
if inputs.top_result_link_count == 0 && rerank < f64::EPSILON && lexical < f64::EPSILON {
return 0.0;
}
let graph_density = (f64::from(inputs.top_result_link_count) / 5.0).min(1.0);
let recency = (-inputs.days_since_top_result_modified / 14.0).exp();
0.50_f64.mul_add(
inputs.top_rerank_score.clamp(0.0, 1.0),
0.20_f64.mul_add(
inputs.top_lexical_indicator.clamp(0.0, 1.0),
0.20_f64.mul_add(
graph_density.clamp(0.0, 1.0),
0.10 * recency.clamp(0.0, 1.0),
),
),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_approx(got: f64, want: f64, label: &str) {
assert!(
(got - want).abs() < 1e-9,
"{label}: got {got:.10}, want {want:.10}"
);
}
#[test]
fn zero_result_returns_zero() {
let inputs = EvidenceInputs {
top_rerank_score: 0.0,
top_lexical_indicator: 0.0,
top_result_link_count: 0,
days_since_top_result_modified: 0.0,
};
assert_approx(compute_evidence_score(&inputs), 0.0, "zero result");
}
#[test]
fn rerank_only_signal() {
let inputs = EvidenceInputs {
top_rerank_score: 0.8,
top_lexical_indicator: 0.0,
top_result_link_count: 0,
days_since_top_result_modified: 0.0,
};
let expected = 0.50_f64.mul_add(0.8, 0.10 * (-0.0_f64 / 14.0_f64).exp());
assert_approx(compute_evidence_score(&inputs), expected, "rerank only");
}
#[test]
fn all_signals_strong_returns_one() {
let inputs = EvidenceInputs {
top_rerank_score: 1.0,
top_lexical_indicator: 1.0,
top_result_link_count: 10,
days_since_top_result_modified: 0.0,
};
assert_approx(compute_evidence_score(&inputs), 1.0, "all strong");
}
}