//! Consensus Lens: Most common value wins. //! //! This lens selects assertions based on how many agents agree on the same value. //! In Phase 2, this counts identical object values across assertions. //! In Phase 4, this will integrate with the Ballot Box vote counts. use crate::traits::{compute_conflict_score, Lens, Resolution}; use std::collections::HashMap; use stemedb_core::types::Assertion; use tracing::instrument; /// Consensus Lens: Returns the assertion representing the most common value. /// /// # Current Implementation (Phase 2 Stub) /// /// Groups assertions by their `object` field (string representation) and /// returns a representative from the largest group. /// /// # Future Implementation (Phase 4) /// /// Will integrate with VoteStore to: /// - Count explicit votes per assertion /// - Weight by agent TrustRank /// - Consider multi-sig counts /// /// # Resolution Strategy /// /// 1. Group assertions by object value /// 2. Select the group with the most members /// 3. From that group, pick the most recent assertion #[derive(Debug, Clone, Copy, Default)] pub struct ConsensusLens; impl Lens for ConsensusLens { #[instrument(skip(self, candidates), fields(candidates_count = candidates.len(), lens = "Consensus"))] 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); } // Group assertions by their object value // We use Debug format as a simple equality proxy let mut groups: HashMap> = HashMap::new(); for assertion in candidates { let key = format!("{:?}", assertion.object); groups.entry(key).or_default().push(assertion); } // Find the largest group let largest_group = groups.values().max_by_key(|v| v.len()); match largest_group { Some(group) => { // From the largest group, pick the most recent let winner = group.iter().max_by_key(|a| a.timestamp).cloned().cloned(); match winner { Some(w) => { let group_size = group.len(); let total = candidates.len(); // Confidence = proportion of candidates that agree let confidence = group_size as f32 / total as f32; let conflict = compute_conflict_score(candidates); Resolution::with_winner(w, total, confidence, conflict) } None => Resolution::empty(), } } None => Resolution::empty(), } } fn name(&self) -> &'static str { "Consensus" } } #[cfg(test)] mod tests { use super::*; use stemedb_core::testing::AssertionBuilder; use stemedb_core::types::ObjectValue; fn create_assertion(subject: &str, value: f64, timestamp: u64) -> Assertion { AssertionBuilder::new().subject(subject).object_number(value).timestamp(timestamp).build() } #[test] fn test_empty_candidates() { let lens = ConsensusLens; let resolution = lens.resolve(&[]); assert!(resolution.winner.is_none()); } #[test] fn test_selects_most_common_value() { let lens = ConsensusLens; // Three assertions say 100.0, one says 200.0 let a1 = create_assertion("Agent1", 100.0, 1000); let a2 = create_assertion("Agent2", 100.0, 1100); let a3 = create_assertion("Agent3", 100.0, 1200); let outlier = create_assertion("Outlier", 200.0, 1300); let resolution = lens.resolve(&[a1, a2, a3, outlier]); assert!(resolution.winner.is_some()); // Winner should have value 100.0 match &resolution.winner.as_ref().map(|a| &a.object) { Some(ObjectValue::Number(v)) => assert!((*v - 100.0).abs() < f64::EPSILON), _ => panic!("Expected Number"), } } #[test] fn test_selects_newest_from_consensus_group() { let lens = ConsensusLens; let old = create_assertion("Old", 100.0, 1000); let new = create_assertion("New", 100.0, 2000); let resolution = lens.resolve(&[old, new.clone()]); // Both have same value, should pick newer assert!(resolution.winner.is_some()); assert_eq!(resolution.winner.as_ref().map(|a| &a.subject), Some(&"New".to_string())); } #[test] fn test_confidence_reflects_agreement() { let lens = ConsensusLens; // 3 out of 4 agree let a1 = create_assertion("A1", 100.0, 1000); let a2 = create_assertion("A2", 100.0, 1100); let a3 = create_assertion("A3", 100.0, 1200); let outlier = create_assertion("Outlier", 200.0, 1300); let resolution = lens.resolve(&[a1, a2, a3, outlier]); // Confidence should be 3/4 = 0.75 assert!((resolution.resolution_confidence - 0.75).abs() < f32::EPSILON); } #[test] fn test_full_consensus() { let lens = ConsensusLens; let a1 = create_assertion("A1", 100.0, 1000); let a2 = create_assertion("A2", 100.0, 1100); let resolution = lens.resolve(&[a1, a2]); // Full consensus = 1.0 confidence assert!((resolution.resolution_confidence - 1.0).abs() < f32::EPSILON); } #[test] fn test_lens_name() { let lens = ConsensusLens; assert_eq!(lens.name(), "Consensus"); } }