use serde::{Deserialize, Serialize};
use crate::{BlockFamily, BlockSummary, ObservabilityStatus, OracleClass, OracleVerdict, Severity};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EndpointVerdict {
pub oracle_class: OracleClass,
pub posterior_probability: f64,
pub verdict: OracleVerdict,
pub severity: Option<Severity>,
pub strategies_run: usize,
pub strategies_total: usize,
pub stop_reason: Option<EndpointStopReason>,
#[serde(skip_serializing_if = "Option::is_none")]
pub first_threshold_crossed_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub final_confirming_strategy: Option<String>,
pub contributing_findings: Vec<ContributingFinding>,
pub observability_status: ObservabilityStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_summary: Option<BlockSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContributingFinding {
pub strategy_id: String,
pub strategy_name: String,
pub outcome_kind: StrategyOutcomeKind,
pub log_odds_contribution: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_family: Option<BlockFamily>,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_reason: Option<String>,
}
impl std::fmt::Display for EndpointStopReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EarlyAccept => write!(f, "EarlyAccept"),
Self::EarlyReject => write!(f, "EarlyReject"),
Self::ExhaustedPlan => write!(f, "ExhaustedPlan"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum StrategyOutcomeKind {
Positive,
NoSignal,
Contradictory,
Inapplicable,
}
impl std::fmt::Display for StrategyOutcomeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Positive => write!(f, "Positive"),
Self::NoSignal => write!(f, "NoSignal"),
Self::Contradictory => write!(f, "Contradictory"),
Self::Inapplicable => write!(f, "Inapplicable"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EndpointStopReason {
EarlyAccept,
EarlyReject,
ExhaustedPlan,
}
#[must_use]
pub fn posterior_to_verdict(p: f64) -> OracleVerdict {
if p >= 0.80 {
OracleVerdict::Confirmed
} else if p >= 0.60 {
OracleVerdict::Likely
} else if p <= 0.20 {
OracleVerdict::NotPresent
} else {
OracleVerdict::Inconclusive
}
}
#[must_use]
pub fn verdict_to_severity(verdict: OracleVerdict) -> Option<Severity> {
match verdict {
OracleVerdict::Confirmed => Some(Severity::High),
OracleVerdict::Likely => Some(Severity::Medium),
OracleVerdict::Inconclusive | OracleVerdict::NotPresent => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn posterior_to_verdict_at_0_80_is_confirmed() {
assert_eq!(posterior_to_verdict(0.80), OracleVerdict::Confirmed);
}
#[test]
fn posterior_to_verdict_at_0_60_is_likely() {
assert_eq!(posterior_to_verdict(0.60), OracleVerdict::Likely);
}
#[test]
fn posterior_to_verdict_at_0_21_is_inconclusive() {
assert_eq!(posterior_to_verdict(0.21), OracleVerdict::Inconclusive);
}
#[test]
fn posterior_to_verdict_at_0_20_is_not_present() {
assert_eq!(posterior_to_verdict(0.20), OracleVerdict::NotPresent);
}
#[test]
fn posterior_to_verdict_at_0_79_is_likely() {
assert_eq!(posterior_to_verdict(0.79), OracleVerdict::Likely);
}
#[test]
fn posterior_to_verdict_at_0_59_is_inconclusive() {
assert_eq!(posterior_to_verdict(0.59), OracleVerdict::Inconclusive);
}
#[test]
fn endpoint_verdict_roundtrip() {
let v = EndpointVerdict {
oracle_class: OracleClass::Existence,
posterior_probability: 0.85,
verdict: OracleVerdict::Confirmed,
severity: Some(Severity::High),
strategies_run: 5,
strategies_total: 10,
stop_reason: Some(EndpointStopReason::EarlyAccept),
first_threshold_crossed_by: None,
final_confirming_strategy: None,
contributing_findings: vec![ContributingFinding {
strategy_id: "existence-get-200-404".to_owned(),
strategy_name: "GET 200/404 existence".to_owned(),
outcome_kind: StrategyOutcomeKind::Positive,
log_odds_contribution: 1.73,
block_family: None,
block_reason: None,
}],
observability_status: ObservabilityStatus::EvidenceObserved,
block_summary: None,
};
let json = serde_json::to_string(&v).expect("serialization failed");
let back: EndpointVerdict = serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(back.strategies_run, 5);
assert_eq!(back.strategies_total, 10);
assert!((back.posterior_probability - 0.85).abs() < f64::EPSILON);
assert_eq!(back.contributing_findings.len(), 1);
}
#[test]
fn endpoint_stop_reason_all_variants_roundtrip() {
for reason in [
EndpointStopReason::EarlyAccept,
EndpointStopReason::EarlyReject,
EndpointStopReason::ExhaustedPlan,
] {
let json = serde_json::to_string(&reason).expect("serialization failed");
let _back: EndpointStopReason =
serde_json::from_str(&json).expect("deserialization failed");
}
}
#[test]
fn endpoint_stop_reason_display() {
assert_eq!(
format!("{}", EndpointStopReason::EarlyAccept),
"EarlyAccept"
);
assert_eq!(
format!("{}", EndpointStopReason::EarlyReject),
"EarlyReject"
);
assert_eq!(
format!("{}", EndpointStopReason::ExhaustedPlan),
"ExhaustedPlan"
);
}
#[test]
fn strategy_outcome_kind_display() {
assert_eq!(format!("{}", StrategyOutcomeKind::Positive), "Positive");
assert_eq!(format!("{}", StrategyOutcomeKind::NoSignal), "NoSignal");
assert_eq!(
format!("{}", StrategyOutcomeKind::Contradictory),
"Contradictory"
);
assert_eq!(
format!("{}", StrategyOutcomeKind::Inapplicable),
"Inapplicable"
);
}
#[test]
fn strategy_outcome_kind_all_variants_roundtrip() {
for kind in [
StrategyOutcomeKind::Positive,
StrategyOutcomeKind::NoSignal,
StrategyOutcomeKind::Contradictory,
StrategyOutcomeKind::Inapplicable,
] {
let json = serde_json::to_string(&kind).expect("serialization failed");
let _back: StrategyOutcomeKind =
serde_json::from_str(&json).expect("deserialization failed");
}
}
}