reputation-core 0.1.0

Core calculation engine for the KnowThat Reputation System with advanced scoring algorithms
Documentation
//! Property-based tests for Phase 2 features

use proptest::prelude::*;
use reputation_core::{Calculator, ConfidenceLevel};
use reputation_types::{AgentData, AgentDataBuilder};
use chrono::{Duration, Utc};

// Strategy for generating valid agent data
fn arb_agent_data() -> impl Strategy<Value = AgentData> {
    (
        any::<u32>(),           // total_interactions
        any::<u32>(),           // total_reviews  
        1.0..=5.0,              // average_rating (must be between 1.0 and 5.0)
        any::<bool>(),          // identity_verified
        any::<bool>(),          // security_audit_passed
        any::<bool>(),          // open_source
        0u8..=3,                // mcp_level
        0..400i64,              // days_old
    ).prop_map(|(interactions, reviews, rating, id, sec, os, mcp, days): (u32, u32, f64, bool, bool, bool, u8, i64)| {
        // Ensure reviews <= interactions
        let reviews = reviews.min(interactions);
        
        let mut builder = AgentDataBuilder::new("did:test:prop")
            .total_interactions(interactions);
        
        // Only set reviews if there are any
        if reviews > 0 {
            let rating_normalized = (rating - 1.0) / 4.0;
            let positive = (rating_normalized * reviews as f64).round() as u32;
            let negative = reviews.saturating_sub(positive);
            builder = builder
                .total_reviews(reviews)
                .positive_reviews(positive)
                .negative_reviews(negative)
                .average_rating(rating);
        }
        
        builder
            .identity_verified(id)
            .security_audit_passed(sec)
            .open_source(os)
            .mcp_level(mcp)
            .created_at(Utc::now() - Duration::days(days))
            .build()
            .unwrap()
    })
}

proptest! {
    #[test]
    fn prop_confidence_level_monotonic(interactions in 0u32..10000) {
        let calc = Calculator::default();
        let conf = interactions as f64 / (interactions as f64 + calc.confidence_k());
        let level = ConfidenceLevel::from_confidence(conf);
        
        // Verify boundaries
        if conf < 0.2 {
            assert_eq!(level, ConfidenceLevel::Low);
        } else if conf < 0.7 {
            assert_eq!(level, ConfidenceLevel::Medium);
        } else {
            assert_eq!(level, ConfidenceLevel::High);
        }
    }
    
    #[test]
    fn prop_is_provisional_consistent_with_confidence(agent in arb_agent_data()) {
        let calc = Calculator::default();
        if let Ok(score) = calc.calculate(&agent) {
            // is_provisional should be true iff confidence < 0.2
            assert_eq!(score.is_provisional, score.confidence < 0.2);
            assert_eq!(score.is_provisional, score.level == ConfidenceLevel::Low);
        }
    }
    
    #[test]
    fn prop_data_points_equals_sum(agent in arb_agent_data()) {
        let calc = Calculator::default();
        if let Ok(score) = calc.calculate(&agent) {
            let expected = agent.total_interactions.saturating_add(agent.total_reviews);
            assert_eq!(score.data_points, expected);
        }
    }
    
    #[test]
    fn prop_prior_breakdown_total_bounded(agent in arb_agent_data()) {
        let calc = Calculator::default();
        if let Ok(score) = calc.calculate(&agent) {
            let breakdown = &score.components.prior_breakdown;
            
            // Total should be sum of components, capped at prior_max
            let sum = breakdown.base_score 
                + breakdown.mcp_bonus 
                + breakdown.identity_bonus
                + breakdown.security_audit_bonus
                + breakdown.open_source_bonus
                + breakdown.age_bonus;
            
            assert_eq!(breakdown.total, sum.min(calc.prior_max()));
            assert!(breakdown.total >= calc.prior_base());
            assert!(breakdown.total <= calc.prior_max());
        }
    }
    
    #[test]
    fn prop_components_match_score_fields(agent in arb_agent_data()) {
        let calc = Calculator::default();
        if let Ok(score) = calc.calculate(&agent) {
            // Components should match top-level score fields
            assert_eq!(score.components.confidence_level, score.level);
            assert_eq!(score.components.confidence_value, score.confidence);
            
            // Prior score in components should match actual prior calculation
            assert_eq!(score.components.prior_score, 
                      score.components.prior_breakdown.total);
        }
    }
    
    #[test]
    fn prop_score_calculation_from_components(agent in arb_agent_data()) {
        let calc = Calculator::default();
        if let Ok(score) = calc.calculate(&agent) {
            // Recalculate score from components
            let recalculated = (1.0 - score.confidence) * score.components.prior_score
                + score.confidence * score.components.empirical_score;
            
            // Should match within floating point precision
            assert!((score.score - recalculated).abs() < 0.0001);
        }
    }
    
    #[test]
    fn prop_mcp_bonus_correct(mcp_level in 0u8..=3, agent in arb_agent_data()) {
        let mut agent = agent;
        agent.mcp_level = Some(mcp_level);
        
        let calc = Calculator::default();
        if let Ok(score) = calc.calculate(&agent) {
            let expected_bonus = match mcp_level {
                1 => 5.0,
                2 => 10.0,
                3 => 15.0,
                _ => 0.0,
            };
            
            assert_eq!(score.components.prior_breakdown.mcp_bonus, expected_bonus);
        }
    }
    
    #[test]
    fn prop_confidence_affects_provisional_status(
        reviews in 0u32..100,
        rating in 1.0..=5.0
    ) {
        let mut builder = AgentDataBuilder::new("did:test:provisional")
            .total_interactions(reviews);
        
        if reviews > 0 {
            builder = builder.with_reviews(reviews, rating);
        }
        
        let agent = builder.build().unwrap();
        
        let calc = Calculator::default();
        let score = calc.calculate(&agent).unwrap();
        
        // Calculate expected confidence
        let expected_conf = reviews as f64 / (reviews as f64 + 15.0);
        
        // Provisional if confidence < 0.2
        let should_be_provisional = expected_conf < 0.2;
        assert_eq!(score.is_provisional, should_be_provisional);
    }
    
    #[test]
    fn prop_batch_preserves_component_details(agents in prop::collection::vec(arb_agent_data(), 1..50)) {
        let calc = Calculator::default();
        
        // Calculate individually
        let individual: Vec<_> = agents.iter()
            .map(|a| calc.calculate(a))
            .collect();
        
        // Calculate in batch
        let batch = calc.calculate_batch(&agents);
        
        // Components should be identical
        for (ind, bat) in individual.iter().zip(batch.iter()) {
            match (ind, bat) {
                (Ok(i), Ok(b)) => {
                    assert_eq!(i.components.prior_score, b.components.prior_score);
                    assert_eq!(i.components.empirical_score, b.components.empirical_score);
                    assert_eq!(i.components.confidence_level, b.components.confidence_level);
                    assert_eq!(i.components.prior_breakdown.total, 
                              b.components.prior_breakdown.total);
                    assert_eq!(i.is_provisional, b.is_provisional);
                    assert_eq!(i.data_points, b.data_points);
                }
                _ => {} // Both should fail or both should succeed
            }
        }
    }
    
    #[test]
    fn prop_builder_produces_valid_components(
        k in 0.1f64..100.0,
        base in 0.0f64..100.0,
        max_offset in 0.1f64..50.0
    ) {
        let max = (base + max_offset).min(100.0);
        
        if let Ok(calc) = Calculator::builder()
            .confidence_k(k)
            .prior_base(base)
            .prior_max(max)
            .build() {
            
            let agent = AgentDataBuilder::new("did:test:builder")
                .with_reviews(50, 4.0)
                .total_interactions(50)
                .build()
                .unwrap();
            
            let score = calc.calculate(&agent).unwrap();
            
            // Components should respect configured bounds
            assert!(score.components.prior_breakdown.base_score >= base);
            assert!(score.components.prior_score <= max);
            assert!(score.components.empirical_score >= 0.0);
            assert!(score.components.empirical_score <= 100.0);
        }
    }
}