stemedb/crates/stemedb-lens/src/confidence.rs
jordan c59066949a feat: Add quickstart "Beyond Hello World" sections with Skeptic and Layered endpoints
- 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>
2026-02-01 21:00:59 -07:00

200 lines
6.7 KiB
Rust

//! 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()));
}
}