Skip to main content

oxibrain_core/
confidence.rs

1//! Confidence calibration (DESIGN §6.5).
2//!
3//! confidence = calibrate(extractor) · corroboration · trust · recency_of_support
4//!
5//! The calibration multiplier is per-extractor, measured by the eval harness.
6//! An unmeasured extractor gets a conservative prior of 0.8.
7
8use serde::{Deserialize, Serialize};
9
10/// Components of the confidence formula (DESIGN §6.5).
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ConfidenceComponents {
13    /// Raw confidence from the LLM assertion.
14    pub raw: f32,
15    /// Per-extractor calibration multiplier [0.1, 2.0].
16    pub calibrated: f32,
17    /// Saturating in distinct supporting episodes [0.5, 1.0].
18    pub corroboration: f32,
19    /// Weighted by episode trust tier [0.3, 1.0].
20    pub trust: f32,
21    /// Recency_of_support for Interval predicates [0.5, 1.0].
22    pub recency: f32,
23}
24
25impl ConfidenceComponents {
26    /// Compute final confidence. Clamped to [0.0, 1.0].
27    pub fn combine(&self) -> f32 {
28        let c = self.raw * self.calibrated * self.corroboration * self.trust * self.recency;
29        c.clamp(0.0, 1.0)
30    }
31}
32
33/// Stores per-extractor calibration values (loaded from eval results).
34#[derive(Debug, Clone, Serialize, Deserialize, Default)]
35pub struct CalibrationTable {
36    pub values: std::collections::BTreeMap<String, f32>,
37}
38
39impl CalibrationTable {
40    /// Look up the calibration multiplier for an extractor.
41    /// Returns `None` for unmeasured extractors; the caller applies the prior.
42    pub fn get(&self, extractor_id: &str) -> Option<f32> {
43        self.values.get(extractor_id).copied()
44    }
45
46    /// Set the calibration multiplier for an extractor. Clamped to [0.1, 2.0].
47    pub fn set(&mut self, extractor_id: &str, value: f32) {
48        self.values
49            .insert(extractor_id.to_string(), value.clamp(0.1, 2.0));
50    }
51
52    /// Serialize to JSON for storage in the `meta` table.
53    pub fn to_json(&self) -> String {
54        serde_json::to_string(self).expect("calibration table serializable")
55    }
56
57    /// Deserialize from JSON (meta table).
58    pub fn from_json(s: &str) -> Self {
59        serde_json::from_str(s).unwrap_or_default()
60    }
61}
62
63/// Per-extractor calibration multiplier. An unmeasured extractor gets a
64/// conservative prior of 0.8.
65pub fn calibrate(extractor_id: &str, table: &CalibrationTable) -> f32 {
66    table.get(extractor_id).unwrap_or(0.8)
67}
68
69/// Derive a calibration multiplier from eval metrics.
70/// Higher precision → higher multiplier (trust the extractor more).
71/// Fabricated entities → penalty.
72pub fn derive_calibration(precision: f64, fabrication_rate: f64) -> f32 {
73    let base = 0.8_f32;
74    let precision_factor = precision as f32;
75    let fabrication_penalty = 1.0 - fabrication_rate as f32;
76    (base * precision_factor * fabrication_penalty).clamp(0.1, 2.0)
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn combine_clamps_to_unit_range() {
85        let c = ConfidenceComponents {
86            raw: 2.0,
87            calibrated: 2.0,
88            corroboration: 2.0,
89            trust: 2.0,
90            recency: 2.0,
91        };
92        assert_eq!(c.combine(), 1.0);
93
94        let c = ConfidenceComponents {
95            raw: 0.0,
96            calibrated: 0.5,
97            corroboration: 0.5,
98            trust: 0.5,
99            recency: 0.5,
100        };
101        assert_eq!(c.combine(), 0.0);
102    }
103
104    #[test]
105    fn calibrate_returns_prior_for_unknown() {
106        let table = CalibrationTable::default();
107        assert_eq!(calibrate("unknown", &table), 0.8);
108    }
109
110    #[test]
111    fn calibrate_returns_stored_value() {
112        let mut table = CalibrationTable::default();
113        table.set("ext1", 1.2);
114        assert_eq!(calibrate("ext1", &table), 1.2);
115    }
116
117    #[test]
118    fn calibration_table_roundtrip() {
119        let mut table = CalibrationTable::default();
120        table.set("ext1", 1.0);
121        table.set("ext2", 0.5);
122        let json = table.to_json();
123        let restored = CalibrationTable::from_json(&json);
124        assert_eq!(restored.get("ext1"), Some(1.0));
125        assert_eq!(restored.get("ext2"), Some(0.5));
126    }
127
128    #[test]
129    fn derive_calibration_monotonic_in_precision() {
130        let low = derive_calibration(0.5, 0.0);
131        let high = derive_calibration(0.95, 0.0);
132        assert!(high > low);
133    }
134
135    #[test]
136    fn derive_calibration_penalizes_fabrication() {
137        let clean = derive_calibration(0.9, 0.0);
138        let dirty = derive_calibration(0.9, 0.1);
139        assert!(clean > dirty);
140    }
141}