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}