Skip to main content

aura_core/effects/
fact.rs

1//! Fact Effect Traits
2//!
3//! Algebraic effects for temporal database operations on facts.
4//! This module provides the mutation/write interface to complement QueryEffects (read).
5//!
6//! # Effect Classification
7//!
8//! - **Category**: Application Effect
9//! - **Implementation**: `aura-effects` (Layer 3) or domain crates
10//! - **Dependencies**: JournalEffects, CryptoEffects (for hashing)
11//!
12//! # Architecture
13//!
14//! FactEffects bridges:
15//! - **Temporal Model**: Datomic-inspired immutable database semantics
16//! - **Journal**: CRDT-based fact storage
17//! - **Finality**: Configurable durability levels
18//! - **Scopes**: Hierarchical namespace organization
19//!
20//! ```text
21//! FactOp (Assert/Retract/EpochBump/Checkpoint)
22//!        ↓
23//! FactEffects::apply_op() → Check scope finality config
24//!        ↓
25//! Apply to journal (CRDT merge or consensus)
26//!        ↓
27//! FactReceipt with current finality level
28//! ```
29//!
30//! # Relationship to QueryEffects
31//!
32//! - `QueryEffects`: Read interface (Datalog queries, subscriptions)
33//! - `FactEffects`: Write interface (temporal mutations, transactions)
34//!
35//! Together they form the complete database interface.
36
37use async_trait::async_trait;
38use serde::{Deserialize, Serialize};
39use std::sync::Arc;
40
41use crate::domain::temporal::{
42    FactOp, FactReceipt, Finality, FinalityError, ScopeFinalityConfig, ScopeId, TemporalPoint,
43    TemporalQuery, Transaction, TransactionReceipt,
44};
45use crate::query::FactId;
46use crate::time::PhysicalTime;
47use crate::Hash32;
48
49pub const MAX_TEMPORAL_FACT_CONTENT_BYTES: usize = 65_536;
50
51// ─────────────────────────────────────────────────────────────────────────────
52// Error Types
53// ─────────────────────────────────────────────────────────────────────────────
54
55/// Error type for fact operations
56#[derive(Debug, Clone, thiserror::Error, Serialize, Deserialize)]
57pub enum FactError {
58    /// Scope not found or not accessible
59    #[error("Scope not found: {scope}")]
60    ScopeNotFound { scope: String },
61
62    /// Fact not found (for retraction/reference operations)
63    #[error("Fact not found: {fact_id:?}")]
64    FactNotFound { fact_id: FactId },
65
66    /// Finality requirement not met
67    #[error("Finality error: {0}")]
68    Finality(#[from] FinalityError),
69
70    /// Transaction conflict (concurrent modification)
71    #[error("Transaction conflict in scope {scope}: {reason}")]
72    TransactionConflict { scope: String, reason: String },
73
74    /// Invalid epoch bump (must be strictly increasing)
75    #[error("Invalid epoch bump: new epoch {new_epoch} not greater than current {current_epoch}")]
76    InvalidEpochBump { current_epoch: u64, new_epoch: u64 },
77
78    /// Authorization failed
79    #[error("Not authorized to modify scope {scope}: {reason}")]
80    NotAuthorized { scope: String, reason: String },
81
82    /// Journal write failed
83    #[error("Journal write failed: {reason}")]
84    JournalError { reason: String },
85
86    /// Handler not available
87    #[error("Fact effect handler not available")]
88    HandlerUnavailable,
89
90    /// Internal error
91    #[error("Internal fact error: {reason}")]
92    Internal { reason: String },
93
94    /// Temporal query error
95    #[error("Temporal query error: {reason}")]
96    TemporalQueryError { reason: String },
97
98    /// Checkpoint not found (for as_of queries)
99    #[error("Checkpoint not found at {point:?}")]
100    CheckpointNotFound { point: String },
101
102    /// Invalid operation
103    #[error("Invalid operation: {reason}")]
104    InvalidOperation { reason: String },
105}
106
107impl FactError {
108    /// Create a scope not found error
109    pub fn scope_not_found(scope: &ScopeId) -> Self {
110        Self::ScopeNotFound {
111            scope: scope.to_string(),
112        }
113    }
114
115    /// Create a fact not found error
116    pub fn fact_not_found(fact_id: FactId) -> Self {
117        Self::FactNotFound { fact_id }
118    }
119
120    /// Create a transaction conflict error
121    pub fn conflict(scope: &ScopeId, reason: impl Into<String>) -> Self {
122        Self::TransactionConflict {
123            scope: scope.to_string(),
124            reason: reason.into(),
125        }
126    }
127
128    /// Create an authorization error
129    pub fn not_authorized(scope: &ScopeId, reason: impl Into<String>) -> Self {
130        Self::NotAuthorized {
131            scope: scope.to_string(),
132            reason: reason.into(),
133        }
134    }
135
136    /// Create a journal error
137    pub fn journal_error(reason: impl Into<String>) -> Self {
138        Self::JournalError {
139            reason: reason.into(),
140        }
141    }
142
143    /// Create an internal error
144    pub fn internal(reason: impl Into<String>) -> Self {
145        Self::Internal {
146            reason: reason.into(),
147        }
148    }
149}
150
151// ─────────────────────────────────────────────────────────────────────────────
152// Fact Effects Trait
153// ─────────────────────────────────────────────────────────────────────────────
154
155/// Effects for temporal database mutations.
156///
157/// This trait provides the write interface for the temporal database,
158/// complementing `QueryEffects` which provides the read interface.
159///
160/// # Operations
161///
162/// - `apply_op`: Apply a single fact operation
163/// - `apply_transaction`: Apply a group of operations atomically
164/// - `wait_for_finality`: Wait for an operation to reach a finality level
165/// - `configure_scope`: Set finality configuration for a scope
166///
167/// # Example
168///
169/// ```ignore
170/// use aura_core::effects::FactEffects;
171/// use aura_core::domain::temporal::{FactOp, FactContent, ScopeId, Finality};
172///
173/// // Simple assertion (no transaction needed)
174/// let receipt = handler.apply_op(
175///     FactOp::assert(FactContent::new("message", content_bytes)),
176///     &ScopeId::parse("authority:abc/chat/channel:xyz")?,
177/// ).await?;
178///
179/// // Wait for replication
180/// handler.wait_for_finality(
181///     receipt.fact_id,
182///     Finality::replicated(3),
183/// ).await?;
184/// ```
185#[async_trait]
186pub trait FactEffects: Send + Sync {
187    /// Apply a single fact operation to a scope.
188    ///
189    /// For simple monotonic operations, this is the preferred method.
190    /// The operation is applied immediately to the local journal and
191    /// replicated according to scope configuration.
192    ///
193    /// # Arguments
194    ///
195    /// * `op` - The operation to apply (Assert, Retract, EpochBump, Checkpoint)
196    /// * `scope` - The scope to apply the operation in
197    ///
198    /// # Returns
199    ///
200    /// A receipt containing the fact ID, timestamp, and initial finality level.
201    async fn apply_op(&self, op: FactOp, scope: &ScopeId) -> Result<FactReceipt, FactError>;
202
203    /// Apply a transaction atomically.
204    ///
205    /// All operations in the transaction succeed or none do.
206    /// The transaction is applied according to its required finality level.
207    ///
208    /// # Arguments
209    ///
210    /// * `transaction` - The transaction containing operations to apply
211    ///
212    /// # Returns
213    ///
214    /// A receipt containing receipts for each operation and transaction finality.
215    async fn apply_transaction(
216        &self,
217        transaction: Transaction,
218    ) -> Result<TransactionReceipt, FactError>;
219
220    /// Wait for a fact to reach a specific finality level.
221    ///
222    /// Blocks until the fact achieves the requested finality or times out.
223    ///
224    /// # Arguments
225    ///
226    /// * `fact_id` - The fact to wait for
227    /// * `target` - The finality level to wait for
228    ///
229    /// # Returns
230    ///
231    /// The achieved finality level (may exceed target).
232    async fn wait_for_finality(
233        &self,
234        fact_id: FactId,
235        target: Finality,
236    ) -> Result<Finality, FactError>;
237
238    /// Get the current finality level of a fact.
239    async fn get_finality(&self, fact_id: FactId) -> Result<Finality, FactError>;
240
241    /// Configure finality requirements for a scope.
242    ///
243    /// Sets default and minimum finality levels, plus content-type overrides.
244    async fn configure_scope(&self, config: ScopeFinalityConfig) -> Result<(), FactError>;
245
246    /// Get the finality configuration for a scope.
247    ///
248    /// Returns the effective configuration, considering inheritance from parent scopes.
249    async fn get_scope_config(&self, scope: &ScopeId) -> Result<ScopeFinalityConfig, FactError>;
250
251    /// Get the current epoch for a scope.
252    async fn get_epoch(&self, scope: &ScopeId) -> Result<crate::types::Epoch, FactError>;
253
254    /// Create a checkpoint for a scope.
255    ///
256    /// Computes the state hash and creates a checkpoint fact.
257    /// Returns the checkpoint fact receipt.
258    async fn checkpoint(&self, scope: &ScopeId) -> Result<FactReceipt, FactError>;
259
260    /// Get the state hash at a specific temporal point.
261    ///
262    /// Used for:
263    /// - Verifying checkpoint integrity
264    /// - Constructing as_of queries
265    /// - Comparing states across time
266    async fn get_state_hash(
267        &self,
268        scope: &ScopeId,
269        point: TemporalPoint,
270    ) -> Result<Hash32, FactError>;
271
272    /// Query facts with temporal semantics.
273    ///
274    /// Executes a query with temporal constraints (as_of, since, history).
275    /// This is a lower-level interface than QueryEffects, returning raw fact data.
276    async fn query_temporal(
277        &self,
278        scope: &ScopeId,
279        temporal: TemporalQuery,
280    ) -> Result<Vec<TemporalFact>, FactError>;
281
282    /// List available checkpoints for a scope.
283    ///
284    /// Returns checkpoints in order, which can be used for as_of queries.
285    async fn list_checkpoints(&self, scope: &ScopeId) -> Result<Vec<CheckpointInfo>, FactError>;
286}
287
288// ─────────────────────────────────────────────────────────────────────────────
289// Supporting Types
290// ─────────────────────────────────────────────────────────────────────────────
291
292/// A fact with temporal metadata for query results.
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct TemporalFact {
295    /// The fact identifier
296    pub fact_id: FactId,
297    /// When the fact was asserted
298    pub asserted_at: PhysicalTime,
299    /// When the fact was retracted (None if still valid)
300    pub retracted_at: Option<PhysicalTime>,
301    /// The scope containing this fact
302    pub scope: ScopeId,
303    /// The epoch when this fact was created
304    pub epoch: crate::types::Epoch,
305    /// The content type
306    pub content_type: String,
307    /// The fact content (serialized)
308    pub content: Vec<u8>,
309    /// Current finality level
310    pub finality: Finality,
311    /// Optional entity ID for entity-based queries
312    pub entity_id: Option<String>,
313}
314
315impl TemporalFact {
316    /// Check if this fact is currently valid (not retracted)
317    pub fn is_valid(&self) -> bool {
318        self.retracted_at.is_none()
319    }
320
321    /// Check if this fact was valid at a specific time
322    pub fn was_valid_at(&self, time: PhysicalTime) -> bool {
323        if self.asserted_at > time {
324            return false;
325        }
326        match &self.retracted_at {
327            Some(retracted) => *retracted > time,
328            None => true,
329        }
330    }
331}
332
333/// Information about a checkpoint
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct CheckpointInfo {
336    /// The checkpoint fact ID
337    pub fact_id: FactId,
338    /// When the checkpoint was created
339    pub created_at: PhysicalTime,
340    /// The state hash at this checkpoint
341    pub state_hash: Hash32,
342    /// The epoch at this checkpoint
343    pub epoch: crate::types::Epoch,
344    /// Number of facts covered by this checkpoint
345    pub fact_count: u32,
346}
347
348// ─────────────────────────────────────────────────────────────────────────────
349// Blanket Implementations
350// ─────────────────────────────────────────────────────────────────────────────
351
352/// Blanket implementation for Arc<T> where T: FactEffects
353#[async_trait]
354impl<T: FactEffects + ?Sized> FactEffects for Arc<T> {
355    async fn apply_op(&self, op: FactOp, scope: &ScopeId) -> Result<FactReceipt, FactError> {
356        (**self).apply_op(op, scope).await
357    }
358
359    async fn apply_transaction(
360        &self,
361        transaction: Transaction,
362    ) -> Result<TransactionReceipt, FactError> {
363        (**self).apply_transaction(transaction).await
364    }
365
366    async fn wait_for_finality(
367        &self,
368        fact_id: FactId,
369        target: Finality,
370    ) -> Result<Finality, FactError> {
371        (**self).wait_for_finality(fact_id, target).await
372    }
373
374    async fn get_finality(&self, fact_id: FactId) -> Result<Finality, FactError> {
375        (**self).get_finality(fact_id).await
376    }
377
378    async fn configure_scope(&self, config: ScopeFinalityConfig) -> Result<(), FactError> {
379        (**self).configure_scope(config).await
380    }
381
382    async fn get_scope_config(&self, scope: &ScopeId) -> Result<ScopeFinalityConfig, FactError> {
383        (**self).get_scope_config(scope).await
384    }
385
386    async fn get_epoch(&self, scope: &ScopeId) -> Result<crate::types::Epoch, FactError> {
387        (**self).get_epoch(scope).await
388    }
389
390    async fn checkpoint(&self, scope: &ScopeId) -> Result<FactReceipt, FactError> {
391        (**self).checkpoint(scope).await
392    }
393
394    async fn get_state_hash(
395        &self,
396        scope: &ScopeId,
397        point: TemporalPoint,
398    ) -> Result<Hash32, FactError> {
399        (**self).get_state_hash(scope, point).await
400    }
401
402    async fn query_temporal(
403        &self,
404        scope: &ScopeId,
405        temporal: TemporalQuery,
406    ) -> Result<Vec<TemporalFact>, FactError> {
407        (**self).query_temporal(scope, temporal).await
408    }
409
410    async fn list_checkpoints(&self, scope: &ScopeId) -> Result<Vec<CheckpointInfo>, FactError> {
411        (**self).list_checkpoints(scope).await
412    }
413}
414
415// ─────────────────────────────────────────────────────────────────────────────
416// Tests
417// ─────────────────────────────────────────────────────────────────────────────
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn test_fact_error_display() {
425        let err = FactError::scope_not_found(&ScopeId::authority("abc"));
426        assert!(err.to_string().contains("authority:abc"));
427
428        let err = FactError::conflict(&ScopeId::root(), "concurrent update");
429        assert!(err.to_string().contains("concurrent update"));
430    }
431
432    fn time_at(ts_ms: u64) -> PhysicalTime {
433        PhysicalTime {
434            ts_ms,
435            uncertainty: None,
436        }
437    }
438
439    #[test]
440    fn test_temporal_fact_validity() {
441        let fact = TemporalFact {
442            fact_id: FactId([0; 32]),
443            asserted_at: time_at(1000),
444            retracted_at: None,
445            scope: ScopeId::root(),
446            epoch: crate::types::Epoch::new(0),
447            content_type: "test".to_string(),
448            content: vec![],
449            finality: Finality::Local,
450            entity_id: None,
451        };
452
453        assert!(fact.is_valid());
454        assert!(fact.was_valid_at(time_at(1500)));
455        assert!(!fact.was_valid_at(time_at(500)));
456    }
457
458    #[test]
459    fn test_temporal_fact_retracted() {
460        let fact = TemporalFact {
461            fact_id: FactId([0; 32]),
462            asserted_at: time_at(1000),
463            retracted_at: Some(time_at(2000)),
464            scope: ScopeId::root(),
465            epoch: crate::types::Epoch::new(0),
466            content_type: "test".to_string(),
467            content: vec![],
468            finality: Finality::Local,
469            entity_id: None,
470        };
471
472        assert!(!fact.is_valid());
473        assert!(fact.was_valid_at(time_at(1500)));
474        assert!(!fact.was_valid_at(time_at(2500)));
475    }
476}