use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Confidence {
Contradicted,
Uncertain,
Inferred,
Verified,
}
impl Confidence {
pub fn decay(self) -> Self {
match self {
Confidence::Verified => Confidence::Verified,
Confidence::Inferred => Confidence::Uncertain,
Confidence::Uncertain => Confidence::Contradicted,
Confidence::Contradicted => Confidence::Contradicted,
}
}
pub fn label(&self) -> &'static str {
match self {
Confidence::Verified => "verified",
Confidence::Inferred => "inferred",
Confidence::Uncertain => "uncertain",
Confidence::Contradicted => "contradicted",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Source {
pub origin: String,
pub recorded_at: DateTime<Utc>,
pub last_verified: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Belief {
pub key: String,
pub value: String,
pub confidence: Confidence,
pub source: Source,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct EpistemicStore {
#[serde(default)]
pub beliefs: HashMap<String, Belief>,
#[serde(default = "default_version")]
pub version: u32,
}
fn default_version() -> u32 {
1
}
impl EpistemicStore {
pub fn new() -> Self {
Self {
beliefs: HashMap::new(),
version: 1,
}
}
pub fn add_belief(
&mut self,
key: &str,
value: &str,
confidence: Confidence,
origin: &str,
) -> ContradictionResult {
let now = Utc::now();
let existing = self.beliefs.get(key).cloned();
if let Some(existing) = existing
&& existing.value != value
&& existing.confidence != Confidence::Contradicted
{
let old_value = existing.value.clone();
let mut contradicted = existing.clone();
contradicted.confidence = Confidence::Contradicted;
contradicted.notes = Some(format!(
"Contradicted by new value '{}' from {} at {}",
value,
origin,
now.format("%Y-%m-%d %H:%M:%S UTC")
));
self.beliefs.insert(
format!("{}:contradicted:{}", key, now.timestamp()),
contradicted,
);
let belief = Belief {
key: key.to_string(),
value: value.to_string(),
confidence,
source: Source {
origin: origin.to_string(),
recorded_at: now,
last_verified: now,
},
notes: None,
};
self.beliefs.insert(key.to_string(), belief);
return ContradictionResult::Contradicted {
old_value,
new_value: value.to_string(),
};
}
let belief = Belief {
key: key.to_string(),
value: value.to_string(),
confidence,
source: Source {
origin: origin.to_string(),
recorded_at: now,
last_verified: now,
},
notes: None,
};
self.beliefs.insert(key.to_string(), belief);
ContradictionResult::NoContradiction
}
pub fn get_belief(&self, key: &str) -> Option<&Belief> {
self.beliefs.get(key)
}
pub fn verify_belief(&mut self, key: &str) -> bool {
if let Some(belief) = self.beliefs.get_mut(key) {
belief.source.last_verified = Utc::now();
belief.confidence = Confidence::Verified;
true
} else {
false
}
}
pub fn apply_decay(&mut self, decay_days: i64) -> Vec<String> {
let now = Utc::now();
let mut decayed = Vec::new();
for belief in self.beliefs.values_mut() {
if belief.confidence == Confidence::Verified {
continue; }
let age_days = (now - belief.source.last_verified).num_days();
if age_days >= decay_days {
let old = belief.confidence;
belief.confidence = belief.confidence.decay();
if belief.confidence != old {
decayed.push(format!(
"{}: {} → {} ({} days since verification)",
belief.key,
old.label(),
belief.confidence.label(),
age_days
));
}
}
}
decayed
}
pub fn list_by_confidence(&self, confidence: Confidence) -> Vec<&Belief> {
self.beliefs
.values()
.filter(|b| b.confidence == confidence)
.collect()
}
pub fn list_contradictions(&self) -> Vec<&Belief> {
self.list_by_confidence(Confidence::Contradicted)
}
pub fn list_by_key_prefix(&self, prefix: &str) -> Vec<&Belief> {
self.beliefs
.values()
.filter(|b| b.key.starts_with(prefix))
.collect()
}
pub fn save(&self, path: &PathBuf) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(self).map_err(std::io::Error::other)?;
std::fs::write(path, content)
}
pub fn load(path: &PathBuf) -> Self {
match std::fs::read_to_string(path) {
Ok(content) => match toml::from_str(&content) {
Ok(store) => store,
Err(e) => {
tracing::warn!("Epistemic store parse error: {}", e);
Self::new()
}
},
Err(_) => Self::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ContradictionResult {
NoContradiction,
Contradicted {
old_value: String,
new_value: String,
},
}
fn epistemic_store_path() -> Option<PathBuf> {
let home = dirs::home_dir()?;
Some(home.join(".opencrabs/brain/epistemic/beliefs.toml"))
}
static STORE: OnceLock<std::sync::Mutex<EpistemicStore>> = OnceLock::new();
fn get_store() -> &'static std::sync::Mutex<EpistemicStore> {
STORE.get_or_init(|| {
let path = epistemic_store_path().expect("home dir must exist");
std::sync::Mutex::new(EpistemicStore::load(&path))
})
}
pub fn add_belief(
key: &str,
value: &str,
confidence: Confidence,
origin: &str,
) -> ContradictionResult {
let store = get_store();
let mut guard = store.lock().expect("epistemic store lock poisoned");
let result = guard.add_belief(key, value, confidence, origin);
if let Some(path) = epistemic_store_path()
&& let Err(e) = guard.save(&path)
{
tracing::warn!("Failed to save epistemic store: {}", e);
}
result
}
pub fn get_belief(key: &str) -> Option<Belief> {
let store = get_store();
let guard = store.lock().expect("epistemic store lock poisoned");
guard.get_belief(key).cloned()
}
pub fn verify_belief(key: &str) -> bool {
let store = get_store();
let mut guard = store.lock().expect("epistemic store lock poisoned");
let result = guard.verify_belief(key);
if result
&& let Some(path) = epistemic_store_path()
&& let Err(e) = guard.save(&path)
{
tracing::warn!("Failed to save epistemic store: {}", e);
}
result
}
pub fn apply_decay(decay_days: i64) -> Vec<String> {
let store = get_store();
let mut guard = store.lock().expect("epistemic store lock poisoned");
let decayed = guard.apply_decay(decay_days);
if !decayed.is_empty()
&& let Some(path) = epistemic_store_path()
&& let Err(e) = guard.save(&path)
{
tracing::warn!("Failed to save epistemic store: {}", e);
}
decayed
}
pub fn list_contradictions() -> Vec<Belief> {
let store = get_store();
let guard = store.lock().expect("epistemic store lock poisoned");
guard.list_contradictions().into_iter().cloned().collect()
}
pub fn list_by_prefix(prefix: &str) -> Vec<Belief> {
let store = get_store();
let guard = store.lock().expect("epistemic store lock poisoned");
guard
.list_by_key_prefix(prefix)
.into_iter()
.cloned()
.collect()
}