use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MemoryStats {
#[serde(default)]
pub total_facts: u64,
#[serde(default)]
pub structured_facts: u64,
#[serde(default)]
pub total_edges: u64,
#[serde(default)]
pub total_retrievals: u64,
#[serde(default)]
pub total_proactive_injections: u64,
#[serde(default)]
pub helpful_retrievals: u64,
#[serde(default)]
pub conflicts_resolved: u64,
#[serde(default)]
pub outstanding_outdated: u64,
#[serde(default)]
pub facts_created: u64,
#[serde(default)]
pub facts_superseded: u64,
}
const TARGET_CONNECTIVITY: f64 = 3.0;
const NEUTRAL: f64 = 0.5;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Dimension {
RepresentationFidelity,
RetrievalPrecision,
UpdateCorrectness,
LongHorizonStability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Bottleneck {
Representation,
Retrieval,
UpdateCorrectness,
LongHorizonStability,
None,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MemorySystemReport {
pub representation_fidelity: f64,
pub retrieval_precision: f64,
pub update_correctness: f64,
pub long_horizon_stability: f64,
pub overall: f64,
pub bottleneck: Bottleneck,
pub recommendation: String,
pub evaluated: bool,
}
fn ratio(numer: u64, denom: u64) -> (f64, bool) {
if denom == 0 {
(NEUTRAL, false)
} else {
((numer as f64 / denom as f64).clamp(0.0, 1.0), true)
}
}
pub fn diagnose(stats: &MemoryStats) -> MemorySystemReport {
let (structured_ratio, has_facts) = ratio(stats.structured_facts, stats.total_facts);
let connectivity = if stats.total_facts == 0 {
NEUTRAL
} else {
(stats.total_edges as f64 / stats.total_facts as f64 / TARGET_CONNECTIVITY).clamp(0.0, 1.0)
};
let representation_fidelity = if has_facts {
0.6 * structured_ratio + 0.4 * connectivity
} else {
NEUTRAL
};
let (retrieval_precision, has_retrievals) =
ratio(stats.helpful_retrievals, stats.total_retrievals);
let (update_correctness, has_updates) = ratio(
stats.conflicts_resolved,
stats.conflicts_resolved + stats.outstanding_outdated,
);
let (long_horizon_stability, has_horizon) = if stats.facts_created == 0 {
(NEUTRAL, false)
} else {
let churn = (stats.facts_superseded as f64 / stats.facts_created as f64).clamp(0.0, 1.0);
(1.0 - churn, true)
};
let overall = (representation_fidelity
+ retrieval_precision
+ update_correctness
+ long_horizon_stability)
/ 4.0;
let candidates = [
(
Dimension::RepresentationFidelity,
representation_fidelity,
has_facts,
),
(
Dimension::RetrievalPrecision,
retrieval_precision,
has_retrievals,
),
(
Dimension::UpdateCorrectness,
update_correctness,
has_updates,
),
(
Dimension::LongHorizonStability,
long_horizon_stability,
has_horizon,
),
];
let evaluated = candidates.iter().any(|(_, _, ev)| *ev);
let bottleneck = candidates
.iter()
.filter(|(_, _, ev)| *ev)
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(dim, _, _)| match dim {
Dimension::RepresentationFidelity => Bottleneck::Representation,
Dimension::RetrievalPrecision => Bottleneck::Retrieval,
Dimension::UpdateCorrectness => Bottleneck::UpdateCorrectness,
Dimension::LongHorizonStability => Bottleneck::LongHorizonStability,
})
.unwrap_or(Bottleneck::None);
MemorySystemReport {
representation_fidelity,
retrieval_precision,
update_correctness,
long_horizon_stability,
overall,
bottleneck,
recommendation: recommend(bottleneck).to_string(),
evaluated,
}
}
fn recommend(b: Bottleneck) -> &'static str {
match b {
Bottleneck::Representation => {
"Invest in representation: enrich fact metadata and graph linkage \
(more structured facts, more edges) so the store captures structure."
}
Bottleneck::Retrieval => {
"Invest in retrieval/routing: tune relevance + utility scoring so \
retrieved memories are the useful ones (raise the helpful ratio)."
}
Bottleneck::UpdateCorrectness => {
"Invest in maintenance: reconcile outdated/conflicting facts (supersede \
or resolve) so dynamic updates land correctly."
}
Bottleneck::LongHorizonStability => {
"Invest in stability: churn is high — prefer localized maintenance over \
global reorganization to reduce supersede thrash."
}
Bottleneck::None => "No evidence yet — exercise the store before diagnosing.",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cold_store_is_all_neutral_and_unevaluated() {
let r = diagnose(&MemoryStats::default());
assert_eq!(r.representation_fidelity, NEUTRAL);
assert_eq!(r.retrieval_precision, NEUTRAL);
assert_eq!(r.update_correctness, NEUTRAL);
assert_eq!(r.long_horizon_stability, NEUTRAL);
assert_eq!(r.bottleneck, Bottleneck::None);
assert!(!r.evaluated);
}
#[test]
fn healthy_store_scores_high() {
let r = diagnose(&MemoryStats {
total_facts: 100,
structured_facts: 95,
total_edges: 300, total_retrievals: 100,
total_proactive_injections: 0,
helpful_retrievals: 90,
conflicts_resolved: 40,
outstanding_outdated: 2,
facts_created: 100,
facts_superseded: 5,
});
assert!(
r.representation_fidelity > 0.9,
"repr={}",
r.representation_fidelity
);
assert!(r.retrieval_precision > 0.85);
assert!(r.update_correctness > 0.9);
assert!(r.long_horizon_stability > 0.9);
assert!(r.overall > 0.85);
assert!(r.evaluated);
}
#[test]
fn bottleneck_is_lowest_dimension_with_evidence() {
let r = diagnose(&MemoryStats {
total_facts: 100,
structured_facts: 90,
total_edges: 300,
total_retrievals: 100,
total_proactive_injections: 0,
helpful_retrievals: 20, conflicts_resolved: 50,
outstanding_outdated: 1,
facts_created: 100,
facts_superseded: 3,
});
assert_eq!(r.bottleneck, Bottleneck::Retrieval);
assert!(r.recommendation.contains("retrieval"));
}
#[test]
fn high_churn_flags_long_horizon_stability() {
let r = diagnose(&MemoryStats {
total_facts: 100,
structured_facts: 95,
total_edges: 300,
total_retrievals: 100,
total_proactive_injections: 0,
helpful_retrievals: 95,
conflicts_resolved: 50,
outstanding_outdated: 1,
facts_created: 100,
facts_superseded: 80, });
assert_eq!(r.bottleneck, Bottleneck::LongHorizonStability);
assert!((r.long_horizon_stability - 0.2).abs() < 1e-9);
assert!(r.recommendation.contains("localized maintenance"));
}
#[test]
fn unevaluated_dimensions_never_chosen_as_bottleneck() {
let r = diagnose(&MemoryStats {
total_retrievals: 10,
total_proactive_injections: 0,
helpful_retrievals: 3, ..Default::default()
});
assert_eq!(r.bottleneck, Bottleneck::Retrieval);
assert!(r.evaluated);
}
#[test]
fn connectivity_is_capped_not_unbounded() {
let dense = diagnose(&MemoryStats {
total_facts: 10,
structured_facts: 10,
total_edges: 100,
..Default::default()
});
assert!((dense.representation_fidelity - 1.0).abs() < 1e-9);
}
}