use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum EvidenceLevel {
#[serde(alias = "A", alias = "a", alias = "proven")]
LevelA,
#[serde(alias = "B", alias = "b", alias = "claimed")]
LevelB,
}
impl EvidenceLevel {
pub fn is_level_a(&self) -> bool {
matches!(self, EvidenceLevel::LevelA)
}
pub fn is_level_b(&self) -> bool {
matches!(self, EvidenceLevel::LevelB)
}
pub fn trust_weight(&self) -> u8 {
match self {
EvidenceLevel::LevelA => 100,
EvidenceLevel::LevelB => 50,
}
}
pub fn requires_proof(&self) -> bool {
matches!(self, EvidenceLevel::LevelA)
}
pub fn sufficient_for_strong_event(&self) -> bool {
matches!(self, EvidenceLevel::LevelA)
}
pub fn code(&self) -> char {
match self {
EvidenceLevel::LevelA => 'A',
EvidenceLevel::LevelB => 'B',
}
}
pub fn from_code(code: char) -> Option<Self> {
match code.to_ascii_uppercase() {
'A' => Some(EvidenceLevel::LevelA),
'B' => Some(EvidenceLevel::LevelB),
_ => None,
}
}
}
impl fmt::Display for EvidenceLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EvidenceLevel::LevelA => write!(f, "level_a"),
EvidenceLevel::LevelB => write!(f, "level_b"),
}
}
}
impl Default for EvidenceLevel {
fn default() -> Self {
EvidenceLevel::LevelB
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum EvidenceSource {
Proven,
Witnessed,
Claimed,
Derived,
Imported,
}
impl EvidenceSource {
pub fn to_level(&self) -> EvidenceLevel {
match self {
EvidenceSource::Proven | EvidenceSource::Witnessed => EvidenceLevel::LevelA,
EvidenceSource::Claimed | EvidenceSource::Derived | EvidenceSource::Imported => {
EvidenceLevel::LevelB
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum DegradationReason {
NetworkPartition {
unreachable_sources: Vec<String>,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
detected_at: chrono::DateTime<chrono::Utc>,
},
ProofSourceUnavailable {
source_id: String,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
last_seen: chrono::DateTime<chrono::Utc>,
},
ProofTimeout { proof_type: String, timeout_ms: u64 },
VerificationFailed {
verification_type: String,
error: String,
},
HistoricalMissing { epoch_id: String, reason: String },
OfflineMode {
#[cfg_attr(feature = "schema", schemars(with = "String"))]
entered_at: chrono::DateTime<chrono::Utc>,
},
EmergencyBypass {
authorized_by: String,
reason: String,
},
}
impl DegradationReason {
pub fn code(&self) -> &'static str {
match self {
DegradationReason::NetworkPartition { .. } => "NET_PARTITION",
DegradationReason::ProofSourceUnavailable { .. } => "SOURCE_UNAVAIL",
DegradationReason::ProofTimeout { .. } => "PROOF_TIMEOUT",
DegradationReason::VerificationFailed { .. } => "VERIFY_FAILED",
DegradationReason::HistoricalMissing { .. } => "HIST_MISSING",
DegradationReason::OfflineMode { .. } => "OFFLINE_MODE",
DegradationReason::EmergencyBypass { .. } => "EMERGENCY",
}
}
pub fn allows_auto_recovery(&self) -> bool {
matches!(
self,
DegradationReason::NetworkPartition { .. }
| DegradationReason::ProofSourceUnavailable { .. }
| DegradationReason::ProofTimeout { .. }
)
}
pub fn requires_human_intervention(&self) -> bool {
matches!(
self,
DegradationReason::VerificationFailed { .. }
| DegradationReason::EmergencyBypass { .. }
)
}
}
impl fmt::Display for DegradationReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.code())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct EvidenceRequirement {
pub min_level: EvidenceLevel,
pub allow_degraded: bool,
pub required_proofs: Vec<String>,
pub is_strong_event: bool,
}
impl EvidenceRequirement {
pub fn strong_event() -> Self {
Self {
min_level: EvidenceLevel::LevelA,
allow_degraded: false,
required_proofs: vec![],
is_strong_event: true,
}
}
pub fn level_a_with_fallback() -> Self {
Self {
min_level: EvidenceLevel::LevelA,
allow_degraded: true,
required_proofs: vec![],
is_strong_event: false,
}
}
pub fn level_b() -> Self {
Self {
min_level: EvidenceLevel::LevelB,
allow_degraded: true,
required_proofs: vec![],
is_strong_event: false,
}
}
pub fn is_satisfied_by(&self, level: EvidenceLevel) -> bool {
level.trust_weight() >= self.min_level.trust_weight()
}
pub fn requires_level_a(&self) -> bool {
self.is_strong_event || self.min_level.is_level_a()
}
}
impl Default for EvidenceRequirement {
fn default() -> Self {
Self::level_b()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_evidence_level_serde() {
assert_eq!(
serde_json::to_string(&EvidenceLevel::LevelA).unwrap(),
"\"level_a\""
);
assert_eq!(
serde_json::to_string(&EvidenceLevel::LevelB).unwrap(),
"\"level_b\""
);
let parsed: EvidenceLevel = serde_json::from_str("\"level_a\"").unwrap();
assert_eq!(parsed, EvidenceLevel::LevelA);
let parsed: EvidenceLevel = serde_json::from_str("\"A\"").unwrap();
assert_eq!(parsed, EvidenceLevel::LevelA);
let parsed: EvidenceLevel = serde_json::from_str("\"B\"").unwrap();
assert_eq!(parsed, EvidenceLevel::LevelB);
let parsed: EvidenceLevel = serde_json::from_str("\"a\"").unwrap();
assert_eq!(parsed, EvidenceLevel::LevelA);
let parsed: EvidenceLevel = serde_json::from_str("\"b\"").unwrap();
assert_eq!(parsed, EvidenceLevel::LevelB);
}
#[test]
fn test_evidence_level_trust_weight() {
assert!(EvidenceLevel::LevelA.trust_weight() > EvidenceLevel::LevelB.trust_weight());
}
#[test]
fn test_evidence_level_codes() {
assert_eq!(EvidenceLevel::LevelA.code(), 'A');
assert_eq!(EvidenceLevel::LevelB.code(), 'B');
assert_eq!(EvidenceLevel::from_code('A'), Some(EvidenceLevel::LevelA));
assert_eq!(EvidenceLevel::from_code('b'), Some(EvidenceLevel::LevelB));
assert_eq!(EvidenceLevel::from_code('X'), None);
}
#[test]
fn test_evidence_level_strong_event() {
assert!(EvidenceLevel::LevelA.sufficient_for_strong_event());
assert!(!EvidenceLevel::LevelB.sufficient_for_strong_event());
}
#[test]
fn test_evidence_requirement_strong() {
let req = EvidenceRequirement::strong_event();
assert!(req.is_satisfied_by(EvidenceLevel::LevelA));
assert!(!req.is_satisfied_by(EvidenceLevel::LevelB));
assert!(req.is_strong_event);
assert!(!req.allow_degraded);
}
#[test]
fn test_evidence_requirement_level_b() {
let req = EvidenceRequirement::level_b();
assert!(req.is_satisfied_by(EvidenceLevel::LevelA));
assert!(req.is_satisfied_by(EvidenceLevel::LevelB));
assert!(!req.is_strong_event);
assert!(req.allow_degraded);
}
#[test]
fn test_evidence_source_to_level() {
assert_eq!(EvidenceSource::Proven.to_level(), EvidenceLevel::LevelA);
assert_eq!(EvidenceSource::Witnessed.to_level(), EvidenceLevel::LevelA);
assert_eq!(EvidenceSource::Claimed.to_level(), EvidenceLevel::LevelB);
assert_eq!(EvidenceSource::Derived.to_level(), EvidenceLevel::LevelB);
assert_eq!(EvidenceSource::Imported.to_level(), EvidenceLevel::LevelB);
}
#[test]
fn test_degradation_reason_codes() {
let reason = DegradationReason::NetworkPartition {
unreachable_sources: vec!["node1".to_string()],
detected_at: chrono::Utc::now(),
};
assert_eq!(reason.code(), "NET_PARTITION");
assert!(reason.allows_auto_recovery());
}
}