soul-base 0.1.0

Data contract primitives for the Soul platform (IDs, Subject, Scope, Consent, Envelope, ...).
Documentation
//! Evidence Types
//!
//! EvidenceLevel classification and DegradationReason per CONST-142, RULE-305~307.
//!
//! # Evidence Gate Principle (CONST-142)
//!
//! Every event must declare its evidence level. The system uses a two-tier
//! classification (A/B) for gate decisions:
//!
//! - **Level A**: Strong evidence - required for strong consequence events
//! - **Level B**: Basic evidence - allowed for regular events

use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "schema")]
use schemars::JsonSchema;

/// Evidence Level Classification (CONST-142, RULE-305)
///
/// Two-tier classification for gate decisions:
/// - Level A: Strong evidence, required for strong consequence events
/// - Level B: Basic evidence, sufficient for regular events
///
/// # Gate Rules (CONST-142)
///
/// - Strong events MUST have Level A evidence
/// - Level B events cannot trigger strong consequences
/// - Degraded mode only allows Level B
///
/// Serde aliases provide compatibility across repos:
/// - Rainbowcore uses `"A"` / `"B"`
/// - Soulseed uses `"level_a"` / `"level_b"` (canonical)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum EvidenceLevel {
    /// Level A: Strong evidence (CONST-142)
    #[serde(alias = "A", alias = "a", alias = "proven")]
    LevelA,

    /// Level B: Basic evidence (CONST-142)
    #[serde(alias = "B", alias = "b", alias = "claimed")]
    LevelB,
}

impl EvidenceLevel {
    /// Check if this is Level A (strong evidence)
    pub fn is_level_a(&self) -> bool {
        matches!(self, EvidenceLevel::LevelA)
    }

    /// Check if this is Level B (basic evidence)
    pub fn is_level_b(&self) -> bool {
        matches!(self, EvidenceLevel::LevelB)
    }

    /// Get the trust weight (0-100)
    pub fn trust_weight(&self) -> u8 {
        match self {
            EvidenceLevel::LevelA => 100,
            EvidenceLevel::LevelB => 50,
        }
    }

    /// Check if this level requires cryptographic proofs
    pub fn requires_proof(&self) -> bool {
        matches!(self, EvidenceLevel::LevelA)
    }

    /// Check if this level is sufficient for strong events (CONST-142)
    pub fn sufficient_for_strong_event(&self) -> bool {
        matches!(self, EvidenceLevel::LevelA)
    }

    /// Get the short code (A or B)
    pub fn code(&self) -> char {
        match self {
            EvidenceLevel::LevelA => 'A',
            EvidenceLevel::LevelB => 'B',
        }
    }

    /// Create from code character
    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
    }
}

/// Evidence source type (for detailed tracking)
///
/// While the main classification is A/B, this provides finer granularity
/// for audit and debugging purposes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum EvidenceSource {
    /// Full cryptographic proof chain (Level A)
    Proven,
    /// Witnessed by trusted party (Level A)
    Witnessed,
    /// Actor claim with identity verification (Level B)
    Claimed,
    /// Derived from other events (Level B unless all sources are A)
    Derived,
    /// Imported from external system (Level B)
    Imported,
}

impl EvidenceSource {
    /// Get the corresponding evidence level
    pub fn to_level(&self) -> EvidenceLevel {
        match self {
            EvidenceSource::Proven | EvidenceSource::Witnessed => EvidenceLevel::LevelA,
            EvidenceSource::Claimed | EvidenceSource::Derived | EvidenceSource::Imported => {
                EvidenceLevel::LevelB
            }
        }
    }
}

/// Degradation Reason (RULE-307)
///
/// When operating in degraded mode, this indicates why full evidence
/// is not available and what compensating controls are in effect.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum DegradationReason {
    /// Network partition - cannot reach proof sources
    NetworkPartition {
        unreachable_sources: Vec<String>,
        #[cfg_attr(feature = "schema", schemars(with = "String"))]
        detected_at: chrono::DateTime<chrono::Utc>,
    },

    /// Proof source unavailable (service down)
    ProofSourceUnavailable {
        source_id: String,
        #[cfg_attr(feature = "schema", schemars(with = "String"))]
        last_seen: chrono::DateTime<chrono::Utc>,
    },

    /// Timeout waiting for proof
    ProofTimeout { proof_type: String, timeout_ms: u64 },

    /// Proof verification failed but continuing in degraded mode
    VerificationFailed {
        verification_type: String,
        error: String,
    },

    /// Historical event without available proofs
    HistoricalMissing { epoch_id: String, reason: String },

    /// Explicitly operating in offline mode
    OfflineMode {
        #[cfg_attr(feature = "schema", schemars(with = "String"))]
        entered_at: chrono::DateTime<chrono::Utc>,
    },

    /// Emergency bypass (requires special authorization)
    EmergencyBypass {
        authorized_by: String,
        reason: String,
    },
}

impl DegradationReason {
    /// Get a short code for the degradation reason
    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",
        }
    }

    /// Check if this reason allows automatic recovery
    pub fn allows_auto_recovery(&self) -> bool {
        matches!(
            self,
            DegradationReason::NetworkPartition { .. }
                | DegradationReason::ProofSourceUnavailable { .. }
                | DegradationReason::ProofTimeout { .. }
        )
    }

    /// Check if this reason requires human intervention
    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())
    }
}

/// Evidence requirement for an event type (CONST-142)
#[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 {
    /// Create a requirement for strong events (Level A, no degradation)
    pub fn strong_event() -> Self {
        Self {
            min_level: EvidenceLevel::LevelA,
            allow_degraded: false,
            required_proofs: vec![],
            is_strong_event: true,
        }
    }

    /// Create a requirement for Level A events (degradation allowed)
    pub fn level_a_with_fallback() -> Self {
        Self {
            min_level: EvidenceLevel::LevelA,
            allow_degraded: true,
            required_proofs: vec![],
            is_strong_event: false,
        }
    }

    /// Create a minimal requirement (Level B is sufficient)
    pub fn level_b() -> Self {
        Self {
            min_level: EvidenceLevel::LevelB,
            allow_degraded: true,
            required_proofs: vec![],
            is_strong_event: false,
        }
    }

    /// Check if a given evidence level satisfies this requirement
    pub fn is_satisfied_by(&self, level: EvidenceLevel) -> bool {
        level.trust_weight() >= self.min_level.trust_weight()
    }

    /// Check if this is a strong event requirement
    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() {
        // Canonical form
        assert_eq!(
            serde_json::to_string(&EvidenceLevel::LevelA).unwrap(),
            "\"level_a\""
        );
        assert_eq!(
            serde_json::to_string(&EvidenceLevel::LevelB).unwrap(),
            "\"level_b\""
        );

        // Roundtrip
        let parsed: EvidenceLevel = serde_json::from_str("\"level_a\"").unwrap();
        assert_eq!(parsed, EvidenceLevel::LevelA);

        // Aliases: Rainbowcore compat
        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);

        // Aliases: lowercase
        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());
    }
}