use crate::error::M1ndResult;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
pub const TRUST_COLD_START_DEFAULT: f32 = 0.5;
pub const RECENCY_HALF_LIFE_HOURS: f32 = 720.0;
pub const RECENCY_FLOOR: f32 = 0.3;
pub const RISK_MULTIPLIER_CAP: f32 = 3.0;
pub const PRIOR_CAP: f32 = 0.95;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TrustEntry {
pub defect_count: u32,
pub false_alarm_count: u32,
pub partial_count: u32,
pub last_defect_timestamp: f64,
pub first_defect_timestamp: f64,
pub total_learn_events: u32,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrustScore {
pub trust_score: f32,
pub defect_density: f32,
pub risk_multiplier: f32,
pub recency_factor: f32,
pub tier: TrustTier,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub enum TrustTier {
HighRisk,
MediumRisk,
LowRisk,
Unknown,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrustNodeOutput {
pub node_id: String,
pub label: String,
pub trust_score: f32,
pub defect_density: f32,
pub risk_multiplier: f32,
pub recency_factor: f32,
pub defect_count: u32,
pub false_alarm_count: u32,
pub partial_count: u32,
pub total_learn_events: u32,
pub last_defect_age_hours: f64,
pub tier: TrustTier,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrustSummary {
pub total_nodes_with_history: u32,
pub high_risk_count: u32,
pub medium_risk_count: u32,
pub low_risk_count: u32,
pub unknown_count: u32,
pub mean_trust: f32,
}
#[derive(Clone, Debug, Serialize)]
pub struct TrustResult {
pub trust_scores: Vec<TrustNodeOutput>,
pub summary: TrustSummary,
pub scope: String,
pub elapsed_ms: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TrustSortBy {
TrustAsc,
TrustDesc,
DefectsDesc,
Recency,
}
impl std::str::FromStr for TrustSortBy {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"trust_desc" => Self::TrustDesc,
"defects_desc" => Self::DefectsDesc,
"recency" => Self::Recency,
_ => Self::TrustAsc,
})
}
}
#[derive(Clone, Debug, Default)]
pub struct TrustLedger {
entries: HashMap<String, TrustEntry>,
}
impl TrustLedger {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn record_defect(&mut self, external_id: &str, timestamp: f64) {
let entry = self
.entries
.entry(external_id.to_string())
.or_insert_with(|| TrustEntry {
defect_count: 0,
false_alarm_count: 0,
partial_count: 0,
last_defect_timestamp: 0.0,
first_defect_timestamp: timestamp,
total_learn_events: 0,
});
entry.defect_count += 1;
entry.total_learn_events += 1;
entry.last_defect_timestamp = timestamp;
if entry.defect_count == 1 {
entry.first_defect_timestamp = timestamp;
}
}
pub fn record_false_alarm(&mut self, external_id: &str, timestamp: f64) {
let entry = self
.entries
.entry(external_id.to_string())
.or_insert_with(|| TrustEntry {
defect_count: 0,
false_alarm_count: 0,
partial_count: 0,
last_defect_timestamp: 0.0,
first_defect_timestamp: 0.0,
total_learn_events: 0,
});
entry.false_alarm_count += 1;
entry.total_learn_events += 1;
let _ = timestamp; }
pub fn record_partial(&mut self, external_id: &str, timestamp: f64) {
let entry = self
.entries
.entry(external_id.to_string())
.or_insert_with(|| TrustEntry {
defect_count: 0,
false_alarm_count: 0,
partial_count: 0,
last_defect_timestamp: 0.0,
first_defect_timestamp: 0.0,
total_learn_events: 0,
});
entry.partial_count += 1;
entry.total_learn_events += 1;
let _ = timestamp;
}
pub fn compute_trust(&self, external_id: &str, now: f64) -> TrustScore {
self.compute_trust_with_params(
external_id,
now,
RECENCY_HALF_LIFE_HOURS,
RISK_MULTIPLIER_CAP,
)
}
pub fn compute_trust_with_params(
&self,
external_id: &str,
now: f64,
half_life_hours: f32,
risk_cap: f32,
) -> TrustScore {
let entry = match self.entries.get(external_id) {
Some(e) => e,
None => {
return TrustScore {
trust_score: TRUST_COLD_START_DEFAULT,
defect_density: 0.0,
risk_multiplier: 1.0,
recency_factor: 0.0,
tier: TrustTier::Unknown,
};
}
};
if entry.total_learn_events == 0 {
return TrustScore {
trust_score: TRUST_COLD_START_DEFAULT,
defect_density: 0.0,
risk_multiplier: 1.0,
recency_factor: 0.0,
tier: TrustTier::Unknown,
};
}
let raw_density = entry.defect_count as f32 / entry.total_learn_events as f32;
let recency = if entry.defect_count > 0 && entry.last_defect_timestamp > 0.0 {
let hours_since = ((now - entry.last_defect_timestamp) / 3600.0).max(0.0) as f32;
(-std::f32::consts::LN_2 * hours_since / half_life_hours.max(1.0)).exp()
} else {
0.0
};
let weighted_density = raw_density * (RECENCY_FLOOR + (1.0 - RECENCY_FLOOR) * recency);
let trust_score = (1.0 - weighted_density).max(0.05);
let risk_multiplier = (1.0 + weighted_density * 2.0).min(risk_cap);
let tier = if trust_score < 0.4 {
TrustTier::HighRisk
} else if trust_score < 0.7 {
TrustTier::MediumRisk
} else {
TrustTier::LowRisk
};
TrustScore {
trust_score,
defect_density: raw_density,
risk_multiplier,
recency_factor: recency,
tier,
}
}
#[allow(clippy::too_many_arguments)]
pub fn report(
&self,
scope: &str,
min_history: u32,
top_k: usize,
node_filter: Option<&str>,
sort_by: TrustSortBy,
now: f64,
half_life_hours: f32,
risk_cap: f32,
) -> TrustResult {
let start = std::time::Instant::now();
let mut outputs: Vec<TrustNodeOutput> = Vec::new();
let mut high_risk_count = 0u32;
let mut medium_risk_count = 0u32;
let mut low_risk_count = 0u32;
let mut unknown_count = 0u32;
let mut trust_sum = 0.0f32;
let mut total_nodes_with_history = 0u32;
for (external_id, entry) in &self.entries {
if scope != "all" {
let matches_scope = match scope {
"file" => external_id.starts_with("file::"),
"module" => {
external_id.starts_with("module::") || external_id.starts_with("dir::")
}
"function" => {
external_id.starts_with("func::") || external_id.starts_with("function::")
}
_ => true,
};
if !matches_scope {
continue;
}
}
if let Some(filter) = node_filter {
if !external_id.contains(filter) {
continue;
}
}
if entry.total_learn_events < min_history {
continue;
}
total_nodes_with_history += 1;
let score = self.compute_trust_with_params(external_id, now, half_life_hours, risk_cap);
match score.tier {
TrustTier::HighRisk => high_risk_count += 1,
TrustTier::MediumRisk => medium_risk_count += 1,
TrustTier::LowRisk => low_risk_count += 1,
TrustTier::Unknown => unknown_count += 1,
}
trust_sum += score.trust_score;
let label = external_id
.rsplit("::")
.next()
.unwrap_or(external_id)
.to_string();
let last_defect_age_hours =
if entry.defect_count > 0 && entry.last_defect_timestamp > 0.0 {
((now - entry.last_defect_timestamp) / 3600.0).max(0.0)
} else {
-1.0 };
outputs.push(TrustNodeOutput {
node_id: external_id.clone(),
label,
trust_score: score.trust_score,
defect_density: score.defect_density,
risk_multiplier: score.risk_multiplier,
recency_factor: score.recency_factor,
defect_count: entry.defect_count,
false_alarm_count: entry.false_alarm_count,
partial_count: entry.partial_count,
total_learn_events: entry.total_learn_events,
last_defect_age_hours,
tier: score.tier,
});
}
match sort_by {
TrustSortBy::TrustAsc => {
outputs.sort_by(|a, b| {
a.trust_score
.partial_cmp(&b.trust_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
TrustSortBy::TrustDesc => {
outputs.sort_by(|a, b| {
b.trust_score
.partial_cmp(&a.trust_score)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
TrustSortBy::DefectsDesc => {
outputs.sort_by(|a, b| b.defect_count.cmp(&a.defect_count));
}
TrustSortBy::Recency => {
outputs.sort_by(|a, b| {
a.last_defect_age_hours
.partial_cmp(&b.last_defect_age_hours)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
}
outputs.truncate(top_k);
let mean_trust = if total_nodes_with_history > 0 {
trust_sum / total_nodes_with_history as f32
} else {
TRUST_COLD_START_DEFAULT
};
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
TrustResult {
trust_scores: outputs,
summary: TrustSummary {
total_nodes_with_history,
high_risk_count,
medium_risk_count,
low_risk_count,
unknown_count,
mean_trust,
},
scope: scope.to_string(),
elapsed_ms,
}
}
pub fn adjust_prior(
&self,
base_prior: f32,
external_ids: &[String],
is_positive_claim: bool,
now: f64,
) -> f32 {
if external_ids.is_empty() {
return base_prior;
}
let mut factor_sum = 0.0f32;
let mut count = 0u32;
for ext_id in external_ids {
let score = self.compute_trust(ext_id, now);
let factor = if is_positive_claim {
score.trust_score
} else {
score.risk_multiplier
};
factor_sum += factor;
count += 1;
}
if count == 0 {
return base_prior;
}
let avg_factor = factor_sum / count as f32;
let adjusted = base_prior * avg_factor;
adjusted.clamp(0.0, PRIOR_CAP)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Serialize, Deserialize)]
struct TrustPersistenceFormat {
version: u32,
entries: HashMap<String, TrustEntry>,
}
pub fn save_trust_state(ledger: &TrustLedger, path: &Path) -> M1ndResult<()> {
let format = TrustPersistenceFormat {
version: 1,
entries: ledger.entries.clone(),
};
let json = serde_json::to_string_pretty(&format).map_err(crate::error::M1ndError::Serde)?;
let temp_path = path.with_extension("tmp");
{
use std::io::Write;
let file = std::fs::File::create(&temp_path)?;
let mut writer = std::io::BufWriter::new(file);
writer.write_all(json.as_bytes())?;
writer.flush()?;
}
std::fs::rename(&temp_path, path)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn make_ledger() -> TrustLedger {
TrustLedger::new()
}
const NOW: f64 = 10_000.0 * 3600.0;
#[test]
fn record_defect_increments_counts() {
let mut ledger = make_ledger();
ledger.record_defect("file::foo.py", NOW);
let entry = ledger.entries.get("file::foo.py").unwrap();
assert_eq!(entry.defect_count, 1);
assert_eq!(entry.total_learn_events, 1);
}
#[test]
fn trust_decreases_with_defects() {
let mut ledger = make_ledger();
let cold = ledger.compute_trust("file::new.py", NOW);
assert_eq!(cold.trust_score, TRUST_COLD_START_DEFAULT);
for i in 0..5 {
ledger.record_defect("file::buggy.py", NOW - i as f64);
}
let buggy = ledger.compute_trust("file::buggy.py", NOW);
assert!(
buggy.trust_score < TRUST_COLD_START_DEFAULT,
"trust_score {} should be below cold start {}",
buggy.trust_score,
TRUST_COLD_START_DEFAULT
);
}
#[test]
fn recency_decay_reduces_old_defects_weight() {
let mut old_ledger = make_ledger();
let mut new_ledger = make_ledger();
let old_ts = NOW - 180.0 * 24.0 * 3600.0;
old_ledger.record_defect("file::module.py", old_ts);
new_ledger.record_defect("file::module.py", NOW);
let old_score = old_ledger.compute_trust("file::module.py", NOW);
let new_score = new_ledger.compute_trust("file::module.py", NOW);
assert!(
old_score.trust_score > new_score.trust_score,
"Old defect should decay: old={} new={}",
old_score.trust_score,
new_score.trust_score
);
}
#[test]
fn risk_multiplier_capped() {
let mut ledger = make_ledger();
for i in 0..50 {
ledger.record_defect("file::broken.py", NOW - i as f64 * 0.1);
}
let score = ledger.compute_trust("file::broken.py", NOW);
assert!(
score.risk_multiplier <= RISK_MULTIPLIER_CAP,
"risk_multiplier {} exceeds cap {}",
score.risk_multiplier,
RISK_MULTIPLIER_CAP
);
}
#[test]
fn report_scope_filters_by_prefix() {
let mut ledger = make_ledger();
ledger.record_defect("file::routes.py", NOW);
ledger.record_defect("module::services", NOW);
let result = ledger.report(
"file",
1,
100,
None,
TrustSortBy::TrustAsc,
NOW,
RECENCY_HALF_LIFE_HOURS,
RISK_MULTIPLIER_CAP,
);
for out in &result.trust_scores {
assert!(
out.node_id.starts_with("file::"),
"Expected file:: prefix, got {}",
out.node_id
);
}
assert!(
!result.trust_scores.is_empty(),
"Should have at least one file:: result"
);
}
#[test]
fn sort_trust_asc_is_ordered() {
let mut ledger = make_ledger();
ledger.record_false_alarm("file::clean.py", NOW);
for i in 0..5 {
ledger.record_defect("file::dirty.py", NOW - i as f64);
}
let result = ledger.report(
"all",
1,
100,
None,
TrustSortBy::TrustAsc,
NOW,
RECENCY_HALF_LIFE_HOURS,
RISK_MULTIPLIER_CAP,
);
let scores: Vec<f32> = result.trust_scores.iter().map(|o| o.trust_score).collect();
for w in scores.windows(2) {
assert!(w[0] <= w[1], "Not sorted ascending: {} > {}", w[0], w[1]);
}
}
#[test]
fn adjust_prior_positive_and_negative_claims() {
let mut ledger = make_ledger();
for i in 0..3 {
ledger.record_defect("file::risky.py", NOW - i as f64 * 60.0);
}
let base = 0.6f32;
let ids = vec!["file::risky.py".to_string()];
let adj_positive = ledger.adjust_prior(base, &ids, true, NOW);
let adj_negative = ledger.adjust_prior(base, &ids, false, NOW);
assert!(
adj_positive <= base,
"Positive claim prior {} should be ≤ base {}",
adj_positive,
base
);
assert!(
adj_negative >= adj_positive,
"Negative claim {} should be ≥ positive {}",
adj_negative,
adj_positive
);
assert!(adj_positive <= PRIOR_CAP);
assert!(adj_negative <= PRIOR_CAP);
}
#[test]
fn save_load_round_trip() {
let mut ledger = make_ledger();
ledger.record_defect("file::persist.py", NOW);
ledger.record_defect("file::persist.py", NOW - 3600.0);
ledger.record_false_alarm("file::persist.py", NOW - 7200.0);
let dir = std::env::temp_dir();
let path: PathBuf = dir.join(format!("trust_test_{}.json", std::process::id()));
save_trust_state(&ledger, &path).expect("save failed");
let loaded = load_trust_state(&path).expect("load failed");
let orig_entry = ledger.entries.get("file::persist.py").unwrap();
let load_entry = loaded.entries.get("file::persist.py").unwrap();
assert_eq!(load_entry.defect_count, orig_entry.defect_count);
assert_eq!(load_entry.false_alarm_count, orig_entry.false_alarm_count);
assert_eq!(load_entry.total_learn_events, orig_entry.total_learn_events);
let _ = std::fs::remove_file(&path);
}
#[test]
fn cold_start_returns_unknown_tier() {
let ledger = make_ledger();
let score = ledger.compute_trust("file::never_seen.py", NOW);
assert_eq!(score.trust_score, TRUST_COLD_START_DEFAULT);
assert_eq!(score.tier, TrustTier::Unknown);
assert_eq!(score.risk_multiplier, 1.0);
}
}
pub fn load_trust_state(path: &Path) -> M1ndResult<TrustLedger> {
if !path.exists() {
return Ok(TrustLedger::new());
}
let data = std::fs::read_to_string(path)?;
let format: TrustPersistenceFormat =
serde_json::from_str(&data).map_err(crate::error::M1ndError::Serde)?;
let mut valid_entries = HashMap::new();
for (key, entry) in format.entries {
if !entry.last_defect_timestamp.is_finite() || !entry.first_defect_timestamp.is_finite() {
eprintln!(
"m1nd trust: rejecting corrupt entry for {}: non-finite timestamps",
key
);
continue;
}
valid_entries.insert(key, entry);
}
Ok(TrustLedger {
entries: valid_entries,
})
}