- Add Layered() method to Go SDK for per-source-class consensus queries - Add LayeredQueryParams, LayeredResult, TierResolution types to Go SDK - Create conflict example demonstrating Skeptic and Layered endpoints - Update quickstart.md with sections 6 (conflict detection) and 7 (authority tiers) - Remove tracked Go binary and add data/ to .gitignore The new quickstart sections demonstrate Episteme's differentiating features: - Skeptic endpoint shows "Trust but Verify" conflict analysis - Layered endpoint shows per-tier resolution (Clinical vs Anecdotal) Note: Pre-existing large files flagged by pre-commit hook (technical debt from prior sessions) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
172 lines
5.5 KiB
Rust
172 lines
5.5 KiB
Rust
//! 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<String, Vec<&Assertion>> = 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");
|
|
}
|
|
}
|