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}