use parlov_core::{StrategyMetaForStop, StrategyOutcome, Vector};
use crate::existence::families::SignalFamily;
use super::reducer::{reduce_all, reduce_with_attribution, EvidenceEvent, ReductionResult};
#[must_use]
pub fn vector_to_family(v: Vector) -> SignalFamily {
match v {
Vector::CacheProbing => SignalFamily::CacheValidator,
Vector::RedirectDiff => SignalFamily::Redirect,
Vector::ErrorMessageGranularity => SignalFamily::ErrorBody,
Vector::StatusCodeDiff => SignalFamily::General,
}
}
#[must_use]
pub fn family_multiplier(count: u8) -> f64 {
match count {
0 => 1.0,
1 => 0.5,
_ => 0.0,
}
}
#[must_use]
pub fn logit(p: f64) -> f64 {
(p / (1.0 - p)).ln()
}
fn logistic(l: f64) -> f64 {
1.0 / (1.0 + (-l).exp())
}
#[must_use]
pub fn confidence_to_prob(confidence: u8) -> f64 {
(f64::from(confidence) / 100.0).clamp(0.01, 0.99)
}
pub struct EvidenceAccumulator {
events: Vec<EvidenceEvent>,
}
impl Default for EvidenceAccumulator {
fn default() -> Self {
Self::new()
}
}
impl EvidenceAccumulator {
#[must_use]
pub fn new() -> Self {
Self { events: Vec::new() }
}
pub fn ingest(&mut self, outcome: &StrategyOutcome, vector: Vector) {
let family = vector_to_family(vector);
match outcome {
StrategyOutcome::Positive(result) => {
if result.confidence < 60 {
tracing::warn!(
confidence = result.confidence,
"Positive outcome below Likely threshold — skipped to avoid negative posterior contribution"
);
return;
}
let p = confidence_to_prob(result.confidence);
let lo = logit(p);
let id = result
.technique_id
.as_deref()
.unwrap_or("unknown")
.to_owned();
self.events
.push(EvidenceEvent::positive(family, id, lo, lo));
}
StrategyOutcome::Contradictory(result, weight) => {
let w = f64::from(*weight);
let id = result
.technique_id
.as_deref()
.unwrap_or("unknown")
.to_owned();
self.events
.push(EvidenceEvent::contradictory(family, id, w, w));
}
StrategyOutcome::NoSignal(_) | StrategyOutcome::Inapplicable(_) => {}
}
}
#[must_use]
pub fn posterior_probability(&self) -> f64 {
logistic(reduce_all(&self.events)).clamp(0.0, 1.0)
}
#[must_use]
pub fn log_odds_current(&self) -> f64 {
reduce_all(&self.events)
}
#[must_use]
pub fn reduce_with_attribution(&self) -> ReductionResult {
reduce_with_attribution(&self.events)
}
#[must_use]
pub fn event_count(&self) -> usize {
self.events.len()
}
#[must_use]
pub fn events(&self) -> &[EvidenceEvent] {
&self.events
}
#[must_use]
pub fn max_positive_remaining(&self, remaining: &[StrategyMetaForStop]) -> f64 {
let current = reduce_all(&self.events);
let mut hypothetical = self.events.clone();
let max_logit = logit(0.99);
for spec in remaining {
let family = vector_to_family(spec.vector);
hypothetical.push(EvidenceEvent::positive(
family,
"max-hypothetical",
max_logit,
max_logit,
));
}
(reduce_all(&hypothetical) - current).max(0.0)
}
#[must_use]
pub fn max_negative_remaining(&self, remaining: &[StrategyMetaForStop]) -> f64 {
let current = reduce_all(&self.events);
let mut hypothetical = self.events.clone();
for spec in remaining {
let max_weight = max_normalization_weight(spec.vector);
if max_weight > 0.0 {
let family = vector_to_family(spec.vector);
hypothetical.push(EvidenceEvent::contradictory(
family,
"max-hypothetical",
max_weight,
max_weight,
));
}
}
(current - reduce_all(&hypothetical)).max(0.0)
}
}
fn max_normalization_weight(v: Vector) -> f64 {
match v {
Vector::StatusCodeDiff => 0.25,
_ => 0.0,
}
}
#[cfg(test)]
#[path = "evidence_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "evidence_contradictory_tests.rs"]
mod contradictory_tests;