//! Tier-aware verdict types for authority-scoped conflict resolution. use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; use crate::types::{TierBreakdown, Verdict}; /// Tier-aware verdict that shows what each tier says about a conflict. /// /// This enables tier-specific conflict resolution where higher-tier authority /// (lower tier number) wins. For example, Tier 1 (RFC) overrides Tier 3 (Expert). #[derive(Debug, Clone, Serialize, Deserialize)] pub enum TierAwareVerdict { /// Single tier conflict (all conflicts from same tier). /// /// This is the common case when all conflicting sources share the same authority tier. SingleTier { /// Tier number (0-5, with 2.5 for TeamPolicy). tier: u8, /// Human-readable tier name (e.g., "Tier 1 (Clinical/RFC)"). tier_name: String, /// The verdict at this tier. verdict: Verdict, /// Number of sources at this tier. sources: usize, /// Maximum confidence among sources at this tier. max_confidence: f32, }, /// Multi-tier conflict (conflicts from multiple tiers). /// /// When conflicts span multiple authority tiers, the primary tier (highest authority, /// lowest tier number) determines the effective verdict. MultiTier { /// Primary tier (lowest tier number = highest authority). primary_tier: u8, /// The verdict from the primary tier (this is the effective verdict). primary_verdict: Verdict, /// Per-tier verdicts: (tier, verdict, source_count, max_confidence). tier_verdicts: Vec<(u8, Verdict, usize, f32)>, /// Overall conflict score. conflict_score: f32, }, /// Higher tier agrees with code (no conflict at high tier, conflict at low tier). /// /// This occurs when a higher-authority tier agrees with the code observation, /// but a lower-authority tier conflicts. The recommendation is to trust the /// higher tier and ignore the lower tier conflict. HigherTierAgreement { /// The tier that agrees with the code. agreeing_tier: u8, /// The tier that conflicts with the code. conflicting_tier: u8, /// Human-readable recommendation. recommendation: String, }, } impl TierAwareVerdict { /// Get the effective verdict (what should be shown to user). /// /// For `SingleTier` and `MultiTier`, this is the verdict. /// For `HigherTierAgreement`, this is `Verdict::Pass` (no action needed). pub fn effective_verdict(&self) -> Verdict { match self { TierAwareVerdict::SingleTier { verdict, .. } => *verdict, TierAwareVerdict::MultiTier { primary_verdict, .. } => *primary_verdict, TierAwareVerdict::HigherTierAgreement { .. } => Verdict::Pass, } } /// Get the primary tier (highest authority tier involved). /// /// Returns the tier number (0-5) of the most authoritative source involved. pub fn primary_tier(&self) -> u8 { match self { TierAwareVerdict::SingleTier { tier, .. } => *tier, TierAwareVerdict::MultiTier { primary_tier, .. } => *primary_tier, TierAwareVerdict::HigherTierAgreement { agreeing_tier, .. } => *agreeing_tier, } } /// Format for display (used in CLI output). /// /// Returns a human-readable string describing the tier-aware verdict. pub fn display(&self) -> String { match self { TierAwareVerdict::SingleTier { tier_name, verdict, sources, max_confidence, .. } => { format!( "{} {} - {} source{}, max confidence {:.2}", verdict.symbol(), tier_name, sources, if *sources == 1 { "" } else { "s" }, max_confidence ) } TierAwareVerdict::MultiTier { primary_tier, primary_verdict, tier_verdicts, conflict_score, } => { let tier_name = format_tier_name(*primary_tier); let mut display = format!( "{} {} (primary tier, score {:.2})", primary_verdict.symbol(), tier_name, conflict_score ); // Add tier breakdown summary if tier_verdicts.len() > 1 { display.push_str(&format!(" - {} tiers involved", tier_verdicts.len())); } display } TierAwareVerdict::HigherTierAgreement { recommendation, .. } => { format!("✓ PASS - {}", recommendation) } } } /// Create a SingleTier verdict from tier breakdown. pub fn from_single_tier(breakdown: &TierBreakdown, verdict: Verdict) -> Self { Self::SingleTier { tier: breakdown.tier, tier_name: format_tier_name(breakdown.tier), verdict, sources: breakdown.assertion_count, max_confidence: breakdown.max_confidence, } } /// Create a MultiTier verdict from tier breakdown map. pub fn from_multi_tier( tier_breakdown: &BTreeMap, primary_tier: u8, primary_verdict: Verdict, conflict_score: f32, ) -> Self { let tier_verdicts: Vec<_> = tier_breakdown .iter() .map(|(tier, bd)| { // Compute what this tier alone would say // For now, we'll use the primary verdict for all tiers // In the future, this could be tier-specific based on thresholds let tier_verdict = if *tier == primary_tier { primary_verdict } else { // Lower-authority tiers might have different verdicts // but for now, we'll keep it simple primary_verdict }; (*tier, tier_verdict, bd.assertion_count, bd.max_confidence) }) .collect(); Self::MultiTier { primary_tier, primary_verdict, tier_verdicts, conflict_score, } } } impl Verdict { /// Get the symbol for this verdict. pub fn symbol(&self) -> &'static str { match self { Verdict::Block => "❌ BLOCK", Verdict::Flag => "⚠️ FLAG", Verdict::Pass => "✓ PASS", Verdict::Ack => "✓ ACK", Verdict::Drift => "🔄 DRIFT", } } } /// Format a tier number as a human-readable name. /// /// Examples: /// - Tier 0 → "Tier 0 (Regulatory)" /// - Tier 1 → "Tier 1 (Clinical/RFC)" /// - Tier 2 → "Tier 2 (Observational)" /// - Tier 2.5 → "Tier 2.5 (TeamPolicy)" /// - Tier 3 → "Tier 3 (Expert)" /// - Tier 4 → "Tier 4 (Community)" /// - Tier 5 → "Tier 5 (Anecdotal)" pub fn format_tier_name(tier: u8) -> String { match tier { 0 => "Tier 0 (Regulatory)".to_string(), 1 => "Tier 1 (Clinical/RFC)".to_string(), 2 => "Tier 2 (Observational/TeamPolicy)".to_string(), 3 => "Tier 3 (Expert)".to_string(), 4 => "Tier 4 (Community)".to_string(), 5 => "Tier 5 (Anecdotal)".to_string(), _ => format!("Tier {tier}"), } } #[cfg(test)] mod tests { use super::*; use stemedb_core::types::SourceClass; #[test] fn test_single_tier_verdict() { let breakdown = TierBreakdown { tier: 1, source_class: SourceClass::Clinical, assertion_count: 3, max_confidence: 0.95, }; let verdict = TierAwareVerdict::from_single_tier(&breakdown, Verdict::Block); assert_eq!(verdict.effective_verdict(), Verdict::Block); assert_eq!(verdict.primary_tier(), 1); let display = verdict.display(); assert!(display.contains("BLOCK")); assert!(display.contains("Tier 1")); assert!(display.contains("3 sources")); } #[test] fn test_multi_tier_verdict() { let mut tier_breakdown = BTreeMap::new(); tier_breakdown.insert( 1, TierBreakdown { tier: 1, source_class: SourceClass::Clinical, assertion_count: 2, max_confidence: 0.95, }, ); tier_breakdown.insert( 3, TierBreakdown { tier: 3, source_class: SourceClass::Expert, assertion_count: 1, max_confidence: 0.70, }, ); let verdict = TierAwareVerdict::from_multi_tier(&tier_breakdown, 1, Verdict::Block, 0.92); assert_eq!(verdict.effective_verdict(), Verdict::Block); assert_eq!(verdict.primary_tier(), 1); let display = verdict.display(); assert!(display.contains("BLOCK")); assert!(display.contains("Tier 1")); assert!(display.contains("2 tiers")); } #[test] fn test_higher_tier_agreement() { let verdict = TierAwareVerdict::HigherTierAgreement { agreeing_tier: 1, conflicting_tier: 4, recommendation: "Tier 1 RFC agrees with your code, ignore Tier 4".to_string(), }; assert_eq!(verdict.effective_verdict(), Verdict::Pass); assert_eq!(verdict.primary_tier(), 1); let display = verdict.display(); assert!(display.contains("PASS")); assert!(display.contains("Tier 1")); } #[test] fn test_primary_tier_is_lowest_number() { let tiers = vec![1u8, 3, 5]; let primary = *tiers.iter().min().unwrap(); assert_eq!(primary, 1); } #[test] fn test_format_tier_name() { assert_eq!(format_tier_name(0), "Tier 0 (Regulatory)"); assert_eq!(format_tier_name(1), "Tier 1 (Clinical/RFC)"); assert_eq!(format_tier_name(2), "Tier 2 (Observational/TeamPolicy)"); assert_eq!(format_tier_name(3), "Tier 3 (Expert)"); assert_eq!(format_tier_name(4), "Tier 4 (Community)"); assert_eq!(format_tier_name(5), "Tier 5 (Anecdotal)"); } #[test] fn test_verdict_symbols() { assert_eq!(Verdict::Block.symbol(), "❌ BLOCK"); assert_eq!(Verdict::Flag.symbol(), "⚠️ FLAG"); assert_eq!(Verdict::Pass.symbol(), "✓ PASS"); assert_eq!(Verdict::Ack.symbol(), "✓ ACK"); assert_eq!(Verdict::Drift.symbol(), "🔄 DRIFT"); } }