Skip to main content

lean_ctx/core/knowledge/
fact.rs

1use chrono::{DateTime, Utc};
2
3use super::types::{COGNITION_SYNTHESIS_SOURCE, FidelityScore, KnowledgeArchetype, KnowledgeFact};
4
5impl KnowledgeFact {
6    pub fn is_current(&self) -> bool {
7        self.valid_until.is_none()
8    }
9
10    /// A synthesized observation: an entity-summary written by the cognition loop's
11    /// synthesis step (#802), not a user-supplied finding. Recall surfaces these as
12    /// orientation (a balanced boost, never absolute).
13    pub fn is_synthesized_observation(&self) -> bool {
14        self.archetype == KnowledgeArchetype::Observation
15            && self.source_session == COGNITION_SYNTHESIS_SOURCE
16    }
17
18    /// Stable, intrinsic quality metric (0.0..1.0).
19    ///
20    /// Based only on confidence, confirmation count, and feedback balance.
21    /// Deliberately excludes volatile signals (retrieval count, recency) to
22    /// keep recall output deterministic. For display ordering use
23    /// `salience_score()` which adds recency and category weighting.
24    pub fn quality_score(&self) -> f32 {
25        let confidence = self.confidence.clamp(0.0, 1.0);
26        let confirmations_norm = (self.confirmation_count.min(5) as f32) / 5.0;
27        let balance = self.feedback_up as i32 - self.feedback_down as i32;
28        let feedback_effect = (balance as f32 / 4.0).tanh() * 0.1;
29
30        // IMPORTANT: quality_score must be stable across repeated recall calls.
31        // Retrieval signals (retrieval_count/last_retrieved) are persisted, but should not change
32        // the displayed "quality" score, otherwise recall output becomes non-deterministic.
33        (0.8 * confidence + 0.2 * confirmations_norm + feedback_effect).clamp(0.0, 1.0)
34    }
35
36    pub fn was_valid_at(&self, at: DateTime<Utc>) -> bool {
37        let after_start = self.valid_from.is_none_or(|from| at >= from);
38        let before_end = self.valid_until.is_none_or(|until| at <= until);
39        after_start && before_end
40    }
41
42    /// Compute structural fidelity score (0.0 - 1.0).
43    /// Based on: has source, confirmations, confidence, freshness, feedback.
44    pub fn compute_structural_fidelity(&self) -> f64 {
45        let mut score: f64 = 0.0;
46        if !self.source_session.is_empty() && self.source_session != "unknown" {
47            score += 0.2;
48        }
49        if self.confirmation_count >= 2 {
50            score += 0.2;
51        }
52        if self.confidence > 0.7 {
53            score += 0.2;
54        }
55        let days_since_confirmed = Utc::now()
56            .signed_duration_since(self.last_confirmed)
57            .num_days();
58        if days_since_confirmed < 14 {
59            score += 0.2;
60        } else if days_since_confirmed < 30 {
61            score += 0.1;
62        }
63        if self.feedback_up > self.feedback_down {
64            score += 0.2;
65        } else if self.feedback_up > 0 {
66            score += 0.1;
67        }
68        score.min(1.0)
69    }
70
71    pub fn update_fidelity(&mut self) {
72        let structural = self.compute_structural_fidelity();
73        self.fidelity = Some(FidelityScore {
74            structural,
75            semantic: structural,
76            computed_at: Utc::now(),
77        });
78    }
79}