- 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>
200 lines
6.7 KiB
Rust
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()));
|
|
}
|
|
}
|