//! Confidence Lens: Highest assertion confidence field wins. //! //! This lens selects assertions based on their self-declared `confidence` field. //! It does NOT consider agent reputation or TrustRank - for reputation-aware //! resolution, use `TrustAwareAuthorityLens`. //! //! # When to Use //! //! Use `ConfidenceLens` when: //! - Assertions have meaningful confidence scores from their sources //! - You want to prefer high-certainty claims over low-certainty ones //! - Agent reputation is not a factor (or is handled elsewhere) //! //! Use `TrustAwareAuthorityLens` when: //! - You want to weight by agent reputation (TrustRank) //! - Agent history matters more than self-declared confidence use crate::traits::{compute_conflict_score, Lens, Resolution}; use stemedb_core::types::Assertion; use tracing::instrument; /// Confidence Lens: Returns the assertion with the highest confidence field. /// /// # Resolution Strategy /// /// 1. Find assertion with maximum `confidence` field value /// 2. If tie: prefer most recent (timestamp tiebreaker) /// /// # Confidence Calculation /// /// Resolution confidence equals the winning assertion's confidence field. /// /// # Example /// /// ```ignore /// use stemedb_lens::{ConfidenceLens, Lens}; /// /// let lens = ConfidenceLens; /// let resolution = lens.resolve(&candidates); /// // Returns assertion with highest confidence field /// ``` #[derive(Debug, Clone, Copy, Default)] pub struct ConfidenceLens; impl Lens for ConfidenceLens { #[instrument(skip(self, candidates), fields(candidates_count = candidates.len(), lens = "Confidence"))] fn resolve(&self, candidates: &[Assertion]) -> Resolution { if candidates.is_empty() { return Resolution::empty(); } if candidates.len() == 1 { return Resolution::with_winner(candidates[0].clone(), 1, 1.0, 0.0); } // Find the assertion with the highest confidence let winner = candidates .iter() .max_by(|a, b| { // Primary: highest confidence // Tiebreaker: most recent timestamp a.confidence .partial_cmp(&b.confidence) .unwrap_or(std::cmp::Ordering::Equal) .then_with(|| a.timestamp.cmp(&b.timestamp)) }) .cloned(); match winner { Some(w) => { // Resolution confidence is the winning assertion's confidence let confidence = w.confidence; let conflict = compute_conflict_score(candidates); Resolution::with_winner(w, candidates.len(), confidence, conflict) } None => Resolution::empty(), } } fn name(&self) -> &'static str { "Confidence" } } #[cfg(test)] mod tests { use super::*; use stemedb_core::testing::AssertionBuilder; fn create_assertion(subject: &str, confidence: f32, timestamp: u64) -> Assertion { AssertionBuilder::new().subject(subject).confidence(confidence).timestamp(timestamp).build() } #[test] fn test_empty_candidates() { let lens = ConfidenceLens; let resolution = lens.resolve(&[]); assert!(resolution.winner.is_none()); } #[test] fn test_selects_highest_confidence() { let lens = ConfidenceLens; let low = create_assertion("Low", 0.3, 1000); let high = create_assertion("High", 0.95, 900); let medium = create_assertion("Medium", 0.6, 1100); let resolution = lens.resolve(&[low, high.clone(), medium]); assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"High".to_string())); } #[test] fn test_tiebreaker_uses_timestamp() { let lens = ConfidenceLens; let older = create_assertion("Older", 0.9, 1000); let newer = create_assertion("Newer", 0.9, 2000); let resolution = lens.resolve(&[older, newer.clone()]); // Same confidence, should pick newer assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"Newer".to_string())); } #[test] fn test_resolution_confidence_matches_winner() { let lens = ConfidenceLens; let high_conf = create_assertion("High", 0.85, 1000); let low_conf = create_assertion("Low", 0.3, 900); let resolution = lens.resolve(&[high_conf, low_conf]); // Resolution confidence should match the winning assertion's confidence assert!((resolution.resolution_confidence - 0.85).abs() < f32::EPSILON); } #[test] fn test_lens_name() { let lens = ConfidenceLens; assert_eq!(lens.name(), "Confidence"); } #[test] fn test_nan_confidence_falls_back_to_timestamp() { let lens = ConfidenceLens; // NaN compared to any value returns Ordering::Equal via unwrap_or // So tiebreaker (timestamp) decides the winner let normal_older = create_assertion("NormalOlder", 0.5, 1000); let mut nan_newer = create_assertion("NaNNewer", 0.0, 2000); nan_newer.confidence = f32::NAN; let resolution = lens.resolve(&[normal_older.clone(), nan_newer]); // NaN == 0.5 (Equal due to unwrap_or), so newer timestamp wins assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"NaNNewer".to_string())); } #[test] fn test_nan_confidence_loses_to_newer_normal() { let lens = ConfidenceLens; // When normal assertion has newer timestamp, it wins (NaN treated as equal) let mut nan_older = create_assertion("NaNOlder", 0.0, 1000); nan_older.confidence = f32::NAN; let normal_newer = create_assertion("NormalNewer", 0.5, 2000); let resolution = lens.resolve(&[nan_older, normal_newer.clone()]); // NaN == 0.5 (Equal), tiebreaker picks newer timestamp assert!(resolution.winner.is_some()); assert_eq!( resolution.winner.as_ref().map(|a| &a.subject), Some(&"NormalNewer".to_string()) ); } #[test] fn test_all_nan_confidence_uses_timestamp() { let lens = ConfidenceLens; let mut older_nan = create_assertion("OlderNaN", 0.0, 1000); older_nan.confidence = f32::NAN; let mut newer_nan = create_assertion("NewerNaN", 0.0, 2000); newer_nan.confidence = f32::NAN; let resolution = lens.resolve(&[older_nan, newer_nan.clone()]); // When all are NaN (equal), tiebreaker should pick newer timestamp assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"NewerNaN".to_string())); } }