lean_ctx/core/context_kernel/
knowledge_health.rs1#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
6pub struct KnowledgeHealthReport {
7 pub total_facts: usize,
9 pub fresh_facts: usize,
11 pub stale_facts: usize,
13 pub contradicted_facts: usize,
15 pub freshness_score: f64,
17 pub contradiction_rate: f64,
19 pub stale_ratio: f64,
21 pub coverage_gaps: usize,
23 pub total_episodes: usize,
25 pub total_procedures: usize,
27}
28
29#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
31pub struct EfficiencyView {
32 pub compression_ratio: f64,
34 pub cache_hit_rate: f64,
36 pub plan_accept_rate: f64,
38 pub kernel_overhead_ms: f64,
40 pub total_tokens_saved: u64,
42 pub total_tokens_sent: u64,
44}
45
46fn ratio(numerator: usize, denominator: usize) -> f64 {
47 if denominator == 0 {
48 0.0
49 } else {
50 numerator as f64 / denominator as f64
51 }
52}
53
54fn ratio_u64(numerator: u64, denominator: u64) -> f64 {
55 if denominator == 0 {
56 0.0
57 } else {
58 numerator as f64 / denominator as f64
59 }
60}
61
62#[must_use]
64pub fn assess_health(
65 facts: &[(bool, bool)],
66 episodes: usize,
67 procedures: usize,
68 coverage_areas: usize,
69 covered_areas: usize,
70) -> KnowledgeHealthReport {
71 let total_facts = facts.len();
72 let fresh_facts = facts.iter().filter(|(fresh, _)| *fresh).count();
73 let stale_facts = total_facts - fresh_facts;
74 let contradicted_facts = facts
75 .iter()
76 .filter(|(_, contradicted)| *contradicted)
77 .count();
78
79 KnowledgeHealthReport {
80 total_facts,
81 fresh_facts,
82 stale_facts,
83 contradicted_facts,
84 freshness_score: ratio(fresh_facts, total_facts),
85 contradiction_rate: ratio(contradicted_facts, total_facts),
86 stale_ratio: ratio(stale_facts, total_facts),
87 coverage_gaps: coverage_areas.saturating_sub(covered_areas),
88 total_episodes: episodes,
89 total_procedures: procedures,
90 }
91}
92
93#[allow(clippy::too_many_arguments)]
95#[must_use]
96pub fn build_efficiency_view(
97 original_tokens: u64,
98 sent_tokens: u64,
99 cache_hits: u64,
100 total_reads: u64,
101 accepted_plans: u64,
102 total_plans: u64,
103 kernel_overhead_ms: f64,
104) -> EfficiencyView {
105 EfficiencyView {
106 compression_ratio: if original_tokens == 0 {
107 0.0
108 } else {
109 1.0 - ratio_u64(sent_tokens, original_tokens)
110 },
111 cache_hit_rate: ratio_u64(cache_hits, total_reads),
112 plan_accept_rate: ratio_u64(accepted_plans, total_plans),
113 kernel_overhead_ms,
114 total_tokens_saved: original_tokens.saturating_sub(sent_tokens),
115 total_tokens_sent: sent_tokens,
116 }
117}
118
119#[must_use]
121pub fn format_org_summary(health: &KnowledgeHealthReport, efficiency: &EfficiencyView) -> String {
122 format!(
123 "Knowledge health\n\
124 Facts: {} total, {} fresh, {} stale, {} contradicted\n\
125 Freshness: {:.1}%\n\
126 Contradictions: {:.1}%\n\
127 Stale: {:.1}%\n\
128 Coverage gaps: {}\n\
129 Episodes: {}\n\
130 Procedures: {}\n\
131 Efficiency\n\
132 Compression: {:.1}%\n\
133 Cache hits: {:.1}%\n\
134 Plan acceptance: {:.1}%\n\
135 Kernel overhead: {:.1} ms\n\
136 Tokens saved: {}\n\
137 Tokens sent: {}",
138 health.total_facts,
139 health.fresh_facts,
140 health.stale_facts,
141 health.contradicted_facts,
142 health.freshness_score * 100.0,
143 health.contradiction_rate * 100.0,
144 health.stale_ratio * 100.0,
145 health.coverage_gaps,
146 health.total_episodes,
147 health.total_procedures,
148 efficiency.compression_ratio * 100.0,
149 efficiency.cache_hit_rate * 100.0,
150 efficiency.plan_accept_rate * 100.0,
151 efficiency.kernel_overhead_ms,
152 efficiency.total_tokens_saved,
153 efficiency.total_tokens_sent,
154 )
155}
156
157#[cfg(test)]
158mod tests {
159 use super::{assess_health, build_efficiency_view, format_org_summary};
160
161 #[test]
162 fn empty_facts_returns_zero_scores() {
163 let report = assess_health(&[], 0, 0, 3, 0);
164 assert_eq!(report.freshness_score, 0.0);
165 assert_eq!(report.contradiction_rate, 0.0);
166 assert_eq!(report.stale_ratio, 0.0);
167 assert_eq!(report.coverage_gaps, 3);
168 }
169
170 #[test]
171 fn all_fresh_facts_score_one() {
172 let report = assess_health(&[(true, false); 10], 0, 0, 0, 0);
173 assert_eq!(report.freshness_score, 1.0);
174 assert_eq!(report.stale_facts, 0);
175 }
176
177 #[test]
178 fn stale_facts_reduce_score() {
179 let mut facts = [(true, false); 10];
180 facts[5..].fill((false, false));
181 let report = assess_health(&facts, 0, 0, 0, 0);
182 assert_eq!(report.freshness_score, 0.5);
183 assert_eq!(report.stale_ratio, 0.5);
184 }
185
186 #[test]
187 fn contradiction_rate_correct() {
188 let mut facts = [(true, false); 10];
189 facts[..2].fill((true, true));
190 let report = assess_health(&facts, 0, 0, 0, 0);
191 assert_eq!(report.contradiction_rate, 0.2);
192 assert_eq!(report.contradicted_facts, 2);
193 }
194
195 #[test]
196 fn efficiency_view_compression() {
197 let view = build_efficiency_view(1_000, 300, 7, 10, 4, 5, 2.5);
198 assert!((view.compression_ratio - 0.7).abs() < f64::EPSILON);
199 assert_eq!(view.total_tokens_saved, 700);
200 assert_eq!(view.total_tokens_sent, 300);
201 }
202
203 #[test]
204 fn format_summary_privacy_safe() {
205 let health = assess_health(&[(true, false)], 2, 3, 4, 1);
206 let efficiency = build_efficiency_view(100, 25, 3, 4, 4, 5, 1.5);
207 let summary = format_org_summary(&health, &efficiency);
208
209 assert!(summary.lines().count() > 1);
210 assert!(!summary.contains("alice@example.com"));
211 assert!(!summary.contains('/'));
212 assert!(summary.contains("75.0%"));
213 }
214}