Skip to main content

mempill_types/
belief.rs

1//! Belief projection: derived, read-time types.
2//!
3//! `BeliefProjection` is never stored; it is recomputed on every `query_memory` call
4//! by performing a canonical valid-time fold over the full claim and assertion history.
5
6use crate::claim::{Confidence, Criticality, Fact};
7use crate::identity::ClaimRef;
8use crate::provenance::ProvenanceLabel;
9use crate::time::{TransactionTime, ValidTime};
10
11// Re-use Cardinality from claim — it's defined there per the design.
12// belief.rs needs Fact, etc., so we import the types from the parent modules.
13
14/// The read-time canonical belief projection.
15///
16/// Derived, never stored. Recomputed on every `query_memory` call by the TruthEngine
17/// performing a canonical valid-time fold. No pre-computed "current value" row exists.
18#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
19pub struct BeliefProjection {
20    /// The resolved belief status (Resolved, Contested, NoBelief, etc.).
21    pub status: BeliefStatus,
22    /// The claim covering NOW under the canonical fold, if unambiguous.
23    pub primary: Option<Belief>,
24    /// Both claims when Contested or Conflict (never silently picked).
25    pub alternatives: Vec<Belief>,
26    /// Derived currency state at read time: Fresh, AgingUnconfirmed, or Decayed.
27    pub currency: CurrencyState,
28    /// Criticality class of the primary claim, or the highest alternative when contested.
29    pub criticality: Criticality,
30    /// Computed staleness flag (is_stale = true when currency is Decayed or no reconfirmation).
31    pub staleness: StalenessFlag,
32    /// Active markers on the projection (Contested, PendingReview, AgedSetMember, etc.).
33    pub markers: Vec<Marker>,
34}
35
36/// Resolved belief status for a subject-line at read time.
37///
38/// Produced by the canonical valid-time fold in `TruthEngine::query_memory`.
39/// The status is authoritative: a `Contested` result means the conflict was
40/// detected and surfaced explicitly, never silently resolved.
41#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
42#[non_exhaustive]
43pub enum BeliefStatus {
44    /// Single live truth.
45    Resolved,
46    /// External contradiction, oracle absent — conflict surfaces explicitly rather than being resolved silently.
47    Contested,
48    /// Multiple mutually-exclusive active beliefs.
49    Conflict,
50    /// Value known, but the valid-time window is unknown (caller did not supply valid-time).
51    TimingUncertain,
52    /// Subject-line exists but no currently-valid claim.
53    NoBelief,
54}
55
56/// A single candidate belief — one arm of the canonical fold result.
57///
58/// A `BeliefProjection` has exactly one `primary` when `Resolved`, two entries in
59/// `alternatives` when `Contested` or `Conflict`, and neither when `NoBelief`.
60#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
61pub struct Belief {
62    /// Stable reference to the underlying committed claim.
63    pub claim_ref: ClaimRef,
64    /// The (subject, predicate, value) triple of the claim.
65    pub fact: Fact,
66    /// Provenance label: who asserted the claim and by what method.
67    pub provenance: ProvenanceLabel,
68    /// Valid-time window of the claim (when it holds in the world).
69    pub valid_time: ValidTime,
70    /// Transaction time: when the claim was written to the store.
71    pub transaction_time: TransactionTime,
72    /// Dual confidence scores (value confidence + valid-time extraction confidence).
73    pub confidence: Confidence,
74    /// Derived currency signal at read time (computed, never stored).
75    pub currency_signal: CurrencySignal,
76    /// Criticality class of this claim.
77    pub criticality: Criticality,
78}
79
80/// Derived currency state at read time.
81///
82/// Computed from `(now - last_refreshed_at)` relative to configured aging thresholds.
83/// Never stored — recomputed on every `query_memory` call.
84#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
85#[non_exhaustive]
86pub enum CurrencyState {
87    /// Within the configured freshness window.
88    Fresh,
89    /// Past the freshness threshold but not yet fully decayed.
90    AgingUnconfirmed,
91    /// Beyond the decay threshold — treat value as potentially stale.
92    Decayed,
93}
94
95/// Currency signal — derived and decaying, refreshed only on provenance-independent restatement.
96///
97/// Currency is not stored; it is computed at read time from `(now - last_refreshed_at)`
98/// relative to the configured aging thresholds. Claims that are not reconfirmed by an
99/// independent source decay over time toward `AgingUnconfirmed` then `Decayed`.
100#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
101pub struct CurrencySignal {
102    /// When this claim's currency was last refreshed (by a provenance-independent restatement).
103    pub last_refreshed_at: TransactionTime,
104    /// Computed decay state at read time (never stored; derived from now - last_refreshed_at).
105    pub state: CurrencyState,
106    /// Number of provenance-independent corroborating sources (confidence annotation only; not a gate).
107    pub corroboration_count: u32,
108}
109
110/// Computed staleness flag on a `BeliefProjection`.
111///
112/// `is_stale` is set when currency is `Decayed` or when the engine's currency
113/// aging thresholds have been exceeded without a provenance-independent reconfirmation.
114#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
115pub struct StalenessFlag {
116    /// Whether the belief is considered stale at read time.
117    pub is_stale: bool,
118    /// Optional human-readable reason for the staleness determination.
119    pub reason: Option<String>,
120}
121
122/// Active signal flags on a `BeliefProjection` at read time.
123///
124/// Multiple markers may be set simultaneously. Callers should inspect all markers,
125/// not just the `status`, for full situational awareness.
126#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
127#[non_exhaustive]
128pub enum Marker {
129    /// The belief is in active contest (two or more unresolved conflicting claims).
130    Contested,
131    /// A conflict exists but neither claim is contested — pending oracle or evidence.
132    PendingConflict,
133    /// A parent claim was superseded; this claim is flagged for human review.
134    PendingReview,
135    /// Set member that has exceeded the currency decay threshold (aging signal).
136    AgedSetMember,
137    /// Claim origin includes RecallReEntry provenance.
138    RecallTainted,
139    /// Derivation depth exceeds the configured cap for currency boosts.
140    LowDerivationAnchor,
141}
142
143/// History entry status for `query_history` — whether the claim is the current belief
144/// or was superseded by a later one.
145///
146/// `Current` and `Superseded` are derived from `is_live` in the canonical fold result
147/// so that `history()` and `recall()` always agree on which entry is current.
148#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
149#[non_exhaustive]
150#[serde(rename_all = "PascalCase")]
151pub enum HistoryEntryStatus {
152    /// This claim is the live (current) belief at the time of the query.
153    Current,
154    /// This claim was superseded by a later claim on the same subject-line.
155    Superseded,
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use chrono::Utc;
162
163    #[test]
164    fn belief_status_round_trip_serde() {
165        let statuses = [
166            BeliefStatus::Resolved,
167            BeliefStatus::Contested,
168            BeliefStatus::Conflict,
169            BeliefStatus::TimingUncertain,
170            BeliefStatus::NoBelief,
171        ];
172        for s in &statuses {
173            let json = serde_json::to_string(s).unwrap();
174            let back: BeliefStatus = serde_json::from_str(&json).unwrap();
175            assert_eq!(s, &back);
176        }
177    }
178
179    #[test]
180    fn currency_state_round_trip_serde() {
181        let states = [CurrencyState::Fresh, CurrencyState::AgingUnconfirmed, CurrencyState::Decayed];
182        for s in &states {
183            let json = serde_json::to_string(s).unwrap();
184            let back: CurrencyState = serde_json::from_str(&json).unwrap();
185            assert_eq!(s, &back);
186        }
187    }
188
189    #[test]
190    fn staleness_flag_not_stale() {
191        let f = StalenessFlag { is_stale: false, reason: None };
192        assert!(!f.is_stale);
193        assert!(f.reason.is_none());
194    }
195
196    #[test]
197    fn marker_round_trip_serde() {
198        let marker = Marker::RecallTainted;
199        let json = serde_json::to_string(&marker).unwrap();
200        let back: Marker = serde_json::from_str(&json).unwrap();
201        assert_eq!(marker, back);
202    }
203
204    #[test]
205    fn currency_signal_round_trip_serde() {
206        let sig = CurrencySignal {
207            last_refreshed_at: TransactionTime(Utc::now()),
208            state: CurrencyState::Fresh,
209            corroboration_count: 3,
210        };
211        let json = serde_json::to_string(&sig).unwrap();
212        let back: CurrencySignal = serde_json::from_str(&json).unwrap();
213        assert_eq!(sig.corroboration_count, back.corroboration_count);
214        assert_eq!(sig.state, back.state);
215    }
216}