use std::fmt::{self, Display, Formatter};
const TRUTH_VALUE_DECIMALS: u32 = 6;
pub const ASSUMED_TRUE_PRIOR: f64 = 0.6;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TruthValue(f64);
impl TruthValue {
pub const FALSE: Self = Self(0.0);
pub const TRUE: Self = Self(1.0);
pub const UNKNOWN: Self = Self(0.5);
#[must_use]
pub fn new(value: f64) -> Self {
if !value.is_finite() {
return Self::UNKNOWN;
}
Self(round_decimal(value.clamp(0.0, 1.0)))
}
#[must_use]
pub const fn get(self) -> f64 {
self.0
}
#[must_use]
pub fn negate(self) -> Self {
Self::new(1.0 - self.0)
}
#[must_use]
pub fn to_decimal_string(self) -> String {
format!("{:.*}", TRUTH_VALUE_DECIMALS as usize, self.0)
}
}
impl Display for TruthValue {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_decimal_string())
}
}
impl From<f64> for TruthValue {
fn from(value: f64) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Aggregator {
Min,
Max,
Average,
Product,
ProbabilisticSum,
}
impl Aggregator {
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Min => "min",
Self::Max => "max",
Self::Average => "average",
Self::Product => "product",
Self::ProbabilisticSum => "probabilistic_sum",
}
}
#[must_use]
pub fn combine(self, values: &[TruthValue]) -> TruthValue {
match self {
Self::Min => values
.iter()
.map(|value| value.0)
.fold(1.0, f64::min)
.into(),
Self::Max => values
.iter()
.map(|value| value.0)
.fold(0.0, f64::max)
.into(),
Self::Average => {
if values.is_empty() {
return TruthValue::UNKNOWN;
}
let sum: f64 = values.iter().map(|value| value.0).sum();
#[allow(clippy::cast_precision_loss)]
let count = values.len() as f64;
TruthValue::new(sum / count)
}
Self::Product => values
.iter()
.map(|value| value.0)
.fold(1.0, |acc, value| acc * value)
.into(),
Self::ProbabilisticSum => probabilistic_sum(values.iter().map(|value| value.0)).into(),
}
}
}
fn probabilistic_sum(values: impl IntoIterator<Item = f64>) -> f64 {
let complement = values
.into_iter()
.fold(1.0, |acc, value| acc * (1.0 - value.clamp(0.0, 1.0)));
1.0 - complement
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceTier {
OriginalFirstParty,
OriginalJournalism,
IndependentCorroboration,
Unoriginal,
}
impl SourceTier {
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::OriginalFirstParty => "original_first_party",
Self::OriginalJournalism => "original_journalism",
Self::IndependentCorroboration => "independent_corroboration",
Self::Unoriginal => "unoriginal",
}
}
#[must_use]
pub const fn weight(self) -> f64 {
match self {
Self::OriginalFirstParty => 1.0,
Self::OriginalJournalism => 0.85,
Self::IndependentCorroboration => 0.5,
Self::Unoriginal => 0.0,
}
}
#[must_use]
pub const fn is_original(self) -> bool {
!matches!(self, Self::Unoriginal)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stance {
Supports,
Contradicts,
Neutral,
}
impl Stance {
#[must_use]
pub const fn slug(self) -> &'static str {
match self {
Self::Supports => "supports",
Self::Contradicts => "contradicts",
Self::Neutral => "neutral",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RelativeEvidence {
pub source_label: String,
pub tier: SourceTier,
pub stance: Stance,
pub strength: TruthValue,
}
impl RelativeEvidence {
#[must_use]
pub fn new(
source_label: impl Into<String>,
tier: SourceTier,
stance: Stance,
strength: impl Into<TruthValue>,
) -> Self {
Self {
source_label: source_label.into(),
tier,
stance,
strength: strength.into(),
}
}
#[must_use]
pub fn effective_mass(&self) -> f64 {
if matches!(self.stance, Stance::Neutral) || !self.tier.is_original() {
return 0.0;
}
self.tier.weight() * self.strength.get()
}
#[must_use]
pub fn is_ignored(&self) -> bool {
self.effective_mass() <= 0.0
}
#[must_use]
pub fn trace_payload(&self) -> String {
format!(
"source={} tier={} stance={} strength={} mass={:.6} ignored={}",
self.source_label,
self.tier.slug(),
self.stance.slug(),
self.strength,
self.effective_mass(),
self.is_ignored(),
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatementAssessment {
pub statement: String,
pub prior: TruthValue,
pub support: TruthValue,
pub contradiction: TruthValue,
pub posterior: TruthValue,
pub ignored_sources: Vec<String>,
}
impl StatementAssessment {
#[must_use]
pub fn assess(
statement: impl Into<String>,
prior: TruthValue,
evidence: &[RelativeEvidence],
) -> Self {
let support = probabilistic_sum(
evidence
.iter()
.filter(|item| matches!(item.stance, Stance::Supports))
.map(RelativeEvidence::effective_mass),
);
let contradiction = probabilistic_sum(
evidence
.iter()
.filter(|item| matches!(item.stance, Stance::Contradicts))
.map(RelativeEvidence::effective_mass),
);
let raised = probabilistic_sum([prior.get(), support]);
let posterior = raised * (1.0 - contradiction);
let ignored_sources = evidence
.iter()
.filter(|item| item.is_ignored())
.map(|item| item.source_label.clone())
.collect();
Self {
statement: statement.into(),
prior,
support: support.into(),
contradiction: contradiction.into(),
posterior: posterior.into(),
ignored_sources,
}
}
#[must_use]
pub fn assess_assumed_true(
statement: impl Into<String>,
evidence: &[RelativeEvidence],
) -> Self {
Self::assess(statement, TruthValue::new(ASSUMED_TRUE_PRIOR), evidence)
}
#[must_use]
pub fn is_probable(&self) -> bool {
self.posterior.get() > 0.5
}
#[must_use]
pub fn trace_payload(&self) -> String {
format!(
"prior={} support={} contradiction={} posterior={} ignored={}",
self.prior,
self.support,
self.contradiction,
self.posterior,
self.ignored_sources.len(),
)
}
}
fn round_decimal(value: f64) -> f64 {
#[allow(clippy::cast_possible_wrap)]
let scale = 10f64.powi(TRUTH_VALUE_DECIMALS as i32);
(value * scale).round() / scale
}