use std::collections::BTreeSet;
pub struct PanelConfidenceInputs {
pub models_agreeing: u32,
pub total_models: u32,
pub converged: bool,
pub rounds_to_convergence: u32,
pub max_rounds: u32,
pub devil_found_serious_objection: bool,
pub minority_reports_count: u32,
}
pub fn canonical_claim_set(claims: &[String]) -> Vec<String> {
claims
.iter()
.map(|claim| claim.trim().to_lowercase())
.filter(|claim| !claim.is_empty())
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
pub fn calculate_convergence(claim_sets: &[Vec<String>]) -> u64 {
if claim_sets.is_empty() {
return 0;
}
let mut all_claims = BTreeSet::new();
let mut model_claims = Vec::new();
for claims in claim_sets {
let canonical = canonical_claim_set(claims);
let set = canonical.into_iter().collect::<BTreeSet<_>>();
for claim in &set {
all_claims.insert(claim.clone());
}
model_claims.push(set);
}
if all_claims.is_empty() {
return 0;
}
if claim_sets.len() == 1 {
return 10000;
}
let total_unique = u64::try_from(all_claims.len()).unwrap_or(0);
let shared_claims = u64::try_from(
all_claims
.iter()
.filter(|c| model_claims.iter().all(|mc| mc.contains(*c)))
.count(),
)
.unwrap_or(0);
(shared_claims * 10000) / total_unique
}
pub fn consensus_claims_at_threshold(
claim_sets: &[Vec<String>],
threshold_bps: u64,
) -> Vec<String> {
if claim_sets.is_empty() {
return Vec::new();
}
let canonical_sets = claim_sets
.iter()
.map(|claims| {
canonical_claim_set(claims)
.into_iter()
.collect::<BTreeSet<_>>()
})
.collect::<Vec<_>>();
let mut all_claims = BTreeSet::new();
for claims in &canonical_sets {
for claim in claims {
all_claims.insert(claim.clone());
}
}
let model_count = u64::try_from(canonical_sets.len()).unwrap_or(0);
if model_count == 0 {
return Vec::new();
}
all_claims
.into_iter()
.filter(|claim| {
let support = u64::try_from(
canonical_sets
.iter()
.filter(|claims| claims.contains(claim))
.count(),
)
.unwrap_or(0);
(support * 10000) / model_count >= threshold_bps
})
.collect()
}
pub fn calculate_panel_confidence(inputs: &PanelConfidenceInputs) -> u64 {
let mut score = 0;
if inputs.total_models > 0 {
let agreeing = std::cmp::min(
u64::from(inputs.models_agreeing),
u64::from(inputs.total_models),
);
let agreement_bps = (agreeing * 5000) / u64::from(inputs.total_models);
score += agreement_bps;
}
if inputs.converged && inputs.max_rounds > 0 {
let r = std::cmp::min(inputs.rounds_to_convergence, inputs.max_rounds);
let remainder = u64::from(inputs.max_rounds)
.saturating_sub(u64::from(r))
.saturating_add(1);
let numerator = std::cmp::min(remainder, u64::from(inputs.max_rounds));
let speed_bps = (numerator * 3000) / u64::from(inputs.max_rounds);
score += speed_bps;
}
if !inputs.devil_found_serious_objection {
score += 2000;
}
score
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod proptests {
use proptest::prelude::*;
use super::*;
proptest! {
#[test]
fn convergence_never_panics_and_bounded(positions in proptest::collection::vec(proptest::collection::vec(".*", 0..10), 0..20)) {
let score = calculate_convergence(&positions);
prop_assert!(score <= 10000, "score {score} out of range");
}
#[test]
fn identical_positions_always_ten_thousand(
pos in proptest::collection::vec("[A-Za-z0-9][A-Za-z0-9 _-]{0,32}", 1..10)
) {
let refs = vec![pos; 5];
let score = calculate_convergence(&refs);
prop_assert_eq!(score, 10000);
}
#[test]
fn panel_confidence_bounded(
models_agreeing in 0u32..=1000,
total_models in 0u32..=1000,
rounds_to_convergence in 0u32..=100,
max_rounds in 0u32..=100,
devil in any::<bool>(),
minority in 0u32..=1000,
) {
let inputs = PanelConfidenceInputs {
models_agreeing,
total_models,
converged: true,
rounds_to_convergence,
max_rounds,
devil_found_serious_objection: devil,
minority_reports_count: minority,
};
let score = calculate_panel_confidence(&inputs);
prop_assert!(score <= 10000, "score {score} exceeds 10000 bps ceiling");
}
}
#[test]
fn empty_positions_returns_zero() {
assert_eq!(calculate_convergence(&[]), 0);
}
#[test]
fn single_position_returns_ten_thousand() {
assert_eq!(calculate_convergence(&[vec!["only one".into()]]), 10000);
}
#[test]
fn all_empty_strings_do_not_panic() {
let score = calculate_convergence(&[Vec::new(), Vec::new(), Vec::new()]);
assert_eq!(score, 0);
}
#[test]
fn disjoint_single_claims_score_zero() {
let score = calculate_convergence(&[
vec!["A".to_string()],
vec!["B".to_string()],
vec!["C".to_string()],
]);
assert_eq!(score, 0);
}
#[test]
fn panel_confidence_does_not_award_speed_without_convergence() {
let converged = PanelConfidenceInputs {
models_agreeing: 3,
total_models: 3,
converged: true,
rounds_to_convergence: 1,
max_rounds: 3,
devil_found_serious_objection: false,
minority_reports_count: 0,
};
let converged_score = calculate_panel_confidence(&converged);
let not_converged = PanelConfidenceInputs {
converged: false,
..converged
};
assert_eq!(converged_score, 10_000);
assert_eq!(calculate_panel_confidence(¬_converged), 7_000);
}
}