use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustTier {
Confirmed,
Suggested,
Unverified,
Ignored,
}
impl TrustTier {
pub fn from_level(level: f64) -> Self {
if level >= 0.8 {
Self::Confirmed
} else if level >= 0.5 {
Self::Suggested
} else if level >= 0.2 {
Self::Unverified
} else {
Self::Ignored
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Confirmed => "hive",
Self::Suggested => "hive, suggested",
Self::Unverified => "hive, unverified",
Self::Ignored => "hive, ignored",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerTrust {
pub peer_id: String,
pub trust_level: f64,
pub knowledge_accepted: u32,
pub knowledge_conflicted: u32,
pub first_seen: u64,
pub last_sync: u64,
}
impl PeerTrust {
pub fn new(peer_id: &str, default_trust: f64) -> Self {
let now = super::epoch_secs();
PeerTrust {
peer_id: peer_id.to_string(),
trust_level: default_trust,
first_seen: now,
last_sync: now,
knowledge_accepted: 0,
knowledge_conflicted: 0,
}
}
pub fn tier(&self) -> TrustTier {
TrustTier::from_level(self.trust_level)
}
pub fn drift_up(&mut self) {
self.trust_level = (self.trust_level + 0.01).min(1.0);
}
pub fn drift_down(&mut self) {
self.trust_level = (self.trust_level - 0.01).max(0.0);
}
pub fn record_accept(&mut self) {
self.knowledge_accepted += 1;
self.last_sync = super::epoch_secs();
}
pub fn record_conflict(&mut self) {
self.knowledge_conflicted += 1;
}
}
pub struct TrustStore {
peers: HashMap<String, PeerTrust>,
default_trust: f64,
}
fn trust_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
PathBuf::from(home)
.join(".claudectl")
.join("hive")
.join("trust.json")
}
impl TrustStore {
pub fn load() -> Self {
Self::load_with_default(0.5)
}
pub fn load_with_default(default_trust: f64) -> Self {
let path = trust_path();
let peers = match fs::read_to_string(&path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
Err(_) => HashMap::new(),
};
TrustStore {
peers,
default_trust,
}
}
pub fn save(&self) -> Result<(), String> {
let path = trust_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("create dir: {e}"))?;
}
let json =
serde_json::to_string_pretty(&self.peers).map_err(|e| format!("serialize: {e}"))?;
fs::write(&path, json).map_err(|e| format!("write: {e}"))
}
pub fn get(&self, peer_id: &str) -> Option<&PeerTrust> {
self.peers.get(peer_id)
}
pub fn get_or_create(&mut self, peer_id: &str) -> &mut PeerTrust {
self.peers
.entry(peer_id.to_string())
.or_insert_with(|| PeerTrust::new(peer_id, self.default_trust))
}
pub fn set_trust(&mut self, peer_id: &str, level: f64) {
let clamped = level.clamp(0.0, 1.0);
let trust = self.get_or_create(peer_id);
trust.trust_level = clamped;
}
pub fn all(&self) -> Vec<&PeerTrust> {
self.peers.values().collect()
}
pub fn record_concordant(&mut self, peer_id: &str) {
let trust = self.get_or_create(peer_id);
trust.drift_up();
trust.record_accept();
}
pub fn record_discordant(&mut self, peer_id: &str) {
let trust = self.get_or_create(peer_id);
trust.drift_down();
trust.record_conflict();
}
pub fn len(&self) -> usize {
self.peers.len()
}
pub fn is_empty(&self) -> bool {
self.peers.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trust_tier_classification() {
assert_eq!(TrustTier::from_level(0.9), TrustTier::Confirmed);
assert_eq!(TrustTier::from_level(0.8), TrustTier::Confirmed);
assert_eq!(TrustTier::from_level(0.79), TrustTier::Suggested);
assert_eq!(TrustTier::from_level(0.5), TrustTier::Suggested);
assert_eq!(TrustTier::from_level(0.49), TrustTier::Unverified);
assert_eq!(TrustTier::from_level(0.2), TrustTier::Unverified);
assert_eq!(TrustTier::from_level(0.19), TrustTier::Ignored);
assert_eq!(TrustTier::from_level(0.0), TrustTier::Ignored);
}
#[test]
fn drift_up_clamped() {
let mut trust = PeerTrust::new("peer-a", 0.99);
trust.drift_up();
assert_eq!(trust.trust_level, 1.0);
trust.drift_up();
assert_eq!(trust.trust_level, 1.0); }
#[test]
fn drift_down_clamped() {
let mut trust = PeerTrust::new("peer-a", 0.01);
trust.drift_down();
assert_eq!(trust.trust_level, 0.0);
trust.drift_down();
assert_eq!(trust.trust_level, 0.0); }
#[test]
fn concordant_discordant_tracking() {
let mut store = TrustStore {
peers: HashMap::new(),
default_trust: 0.5,
};
store.record_concordant("peer-a");
store.record_concordant("peer-a");
store.record_discordant("peer-a");
let trust = store.get("peer-a").unwrap();
assert_eq!(trust.knowledge_accepted, 2);
assert_eq!(trust.knowledge_conflicted, 1);
assert!((trust.trust_level - 0.51).abs() < 0.001);
}
#[test]
fn set_trust_clamped() {
let mut store = TrustStore {
peers: HashMap::new(),
default_trust: 0.5,
};
store.set_trust("peer-a", 1.5);
assert_eq!(store.get("peer-a").unwrap().trust_level, 1.0);
store.set_trust("peer-b", -0.5);
assert_eq!(store.get("peer-b").unwrap().trust_level, 0.0);
}
#[test]
fn get_or_create_default() {
let mut store = TrustStore {
peers: HashMap::new(),
default_trust: 0.7,
};
let trust = store.get_or_create("new-peer");
assert_eq!(trust.trust_level, 0.7);
assert_eq!(trust.peer_id, "new-peer");
}
#[test]
fn tier_labels() {
assert_eq!(TrustTier::Confirmed.label(), "hive");
assert_eq!(TrustTier::Suggested.label(), "hive, suggested");
assert_eq!(TrustTier::Unverified.label(), "hive, unverified");
assert_eq!(TrustTier::Ignored.label(), "hive, ignored");
}
#[test]
fn peer_trust_serde_roundtrip() {
let trust = PeerTrust::new("peer-a", 0.8);
let json = serde_json::to_string(&trust).unwrap();
let back: PeerTrust = serde_json::from_str(&json).unwrap();
assert_eq!(back.peer_id, "peer-a");
assert_eq!(back.trust_level, 0.8);
}
}