//! Storage operations for LocalEpisteme. //! //! Handles ingestion of claims, observations, and authoritative assertions. use stemedb_core::types::Assertion; use stemedb_ingest::serialize_assertion; use stemedb_storage::PredicateIndexStore; use tracing::{debug, info, instrument, warn}; use crate::bridge::{claim_to_assertion, observation_to_assertion}; use crate::types::{predicates, Observation}; use crate::walker::git::get_current_commit_hash; use crate::AphoriaError; use super::super::corpus::current_timestamp; use super::LocalEpisteme; impl LocalEpisteme { /// Ingest a batch of extracted claims into Episteme. #[instrument(skip(self, claims), fields(claim_count = claims.len()))] pub async fn ingest_claims(&self, claims: &[Observation]) -> Result { let timestamp = current_timestamp(); let mut ingested = 0; // Capture current git commit hash let git_commit = get_current_commit_hash(&self.project_root); if let Some(ref hash) = git_commit { debug!(git_commit = %hash, "Captured git commit for claim ingestion"); } // Collect claims for predicate index updates let mut acknowledged_claims = Vec::new(); let mut blessed_claims = Vec::new(); for claim in claims { let assertion = claim_to_assertion(claim, &self.signing_key, timestamp, git_commit.as_deref()); // Serialize and write to WAL let record_bytes = serialize_assertion(&assertion) .map_err(|e| AphoriaError::Storage(format!("Failed to serialize claim: {e}")))?; // Compute hash for predicate indexing (same as Ingestor uses) let hash = *blake3::hash(&record_bytes[8..]).as_bytes(); // Skip 8-byte header let mut journal = self.journal.lock().await; journal.append(record_bytes).map_err(|e| { AphoriaError::Storage(format!("Failed to append claim to WAL: {e}")) })?; // Track acknowledged claims for predicate index update if claim.predicate == predicates::ACKNOWLEDGED { acknowledged_claims.push(hash); } // Track blessed claims (created via `bless` command) for predicate index if claim.file == "aphoria_bless" { blessed_claims.push(hash); } debug!( concept_path = %claim.concept_path, predicate = %claim.predicate, "Ingested claim" ); ingested += 1; } // Sync WAL { let mut journal = self.journal.lock().await; journal .force_sync() .map_err(|e| AphoriaError::Storage(format!("Failed to sync claims WAL: {e}")))?; } // Wait for ingestion to process self.ingestor.process_pending().await.map_err(|e| { AphoriaError::Storage(format!("Failed to process claims ingestion: {e}")) })?; // Update predicate index for acknowledged claims for hash in acknowledged_claims { if let Err(e) = self .predicate_index_store .add_to_predicate_index(predicates::ACKNOWLEDGED, &hash) .await { warn!(hash = %hex::encode(hash), error = %e, "Failed to add to predicate index"); } } // Update predicate index for blessed claims for hash in blessed_claims { if let Err(e) = self.predicate_index_store.add_to_predicate_index(predicates::BLESSED, &hash).await { warn!(hash = %hex::encode(hash), error = %e, "Failed to add to blessed index"); } } info!(ingested, "Ingested claims into Episteme"); Ok(ingested) } /// Ingest code claims as Tier 4 (Community) observations. /// /// Used for claims that have no authority conflict — these become "project memory" /// that persists across commits and enables future drift detection. /// /// Returns the number of observations successfully ingested. #[instrument(skip(self, observations), fields(count = observations.len()))] pub async fn ingest_observations( &self, observations: &[Observation], ) -> Result { if observations.is_empty() { return Ok(0); } let timestamp = current_timestamp(); // Capture current git commit hash let git_commit = get_current_commit_hash(&self.project_root); if let Some(ref hash) = git_commit { debug!(git_commit = %hash, "Captured git commit for observation ingestion"); } let mut count = 0; for claim in observations { let assertion = observation_to_assertion(claim, &self.signing_key, timestamp, git_commit.as_deref()); // Serialize and write to WAL let record_bytes = serialize_assertion(&assertion).map_err(|e| { AphoriaError::Storage(format!("Failed to serialize observation: {e}")) })?; // Compute hash for predicate indexing let hash = *blake3::hash(&record_bytes[8..]).as_bytes(); // Skip 8-byte header let mut journal = self.journal.lock().await; journal.append(record_bytes).map_err(|e| { AphoriaError::Storage(format!("Failed to append observation to WAL: {e}")) })?; drop(journal); // Add to predicate index for "observation" queries if let Err(e) = self .predicate_index_store .add_to_predicate_index(predicates::OBSERVATION, &hash) .await { warn!(hash = %hex::encode(hash), error = %e, "Failed to add to observation index"); } debug!( concept_path = %claim.concept_path, predicate = %claim.predicate, "Ingested observation" ); count += 1; } // Sync WAL { let mut journal = self.journal.lock().await; journal.force_sync().map_err(|e| { AphoriaError::Storage(format!("Failed to sync observations WAL: {e}")) })?; } // Wait for ingestion to process self.ingestor.process_pending().await.map_err(|e| { AphoriaError::Storage(format!("Failed to process observations ingestion: {e}")) })?; info!(count, "Ingested observations as Tier 4 (project memory)"); Ok(count) } /// Ingest authoritative assertions (RFC, OWASP, etc.). /// /// Writes assertions to WAL and adds them to the AUTHORITATIVE predicate index /// so they are discoverable by `fetch_authoritative_assertions()` during scans. #[instrument(skip(self, assertions), fields(count = assertions.len()))] pub async fn ingest_authoritative( &self, assertions: &[Assertion], ) -> Result { let mut ingested = 0; let mut hashes = Vec::with_capacity(assertions.len()); for assertion in assertions { let record_bytes = serialize_assertion(assertion).map_err(|e| { AphoriaError::Storage(format!( "Failed to serialize authoritative assertion '{}': {e}", assertion.subject )) })?; // Compute hash for predicate indexing (skip 8-byte header, same as Ingestor) let hash = *blake3::hash(&record_bytes[8..]).as_bytes(); hashes.push(hash); let mut journal = self.journal.lock().await; journal.append(record_bytes).map_err(|e| { AphoriaError::Storage(format!( "Failed to append authoritative assertion to WAL: {e}" )) })?; ingested += 1; } // Sync and process { let mut journal = self.journal.lock().await; journal.force_sync().map_err(|e| { AphoriaError::Storage(format!("Failed to sync authoritative WAL: {e}")) })?; } self.ingestor.process_pending().await.map_err(|e| { AphoriaError::Storage(format!("Failed to process authoritative ingestion: {e}")) })?; // Add all assertions to the AUTHORITATIVE predicate index // This mirrors the pattern from policy_ops.rs import_policy() for hash in &hashes { if let Err(e) = self .predicate_index_store .add_to_predicate_index(predicates::AUTHORITATIVE, hash) .await { warn!(hash = %hex::encode(hash), error = %e, "Failed to add to authoritative index"); } } info!(ingested, "Ingested authoritative assertions"); Ok(ingested) } /// Fetch all "acknowledged" assertions for policy export. pub async fn fetch_acknowledgments(&self) -> Result, AphoriaError> { self.fetch_assertions_by_predicate(predicates::ACKNOWLEDGED).await } /// Fetch all "blessed" assertions (authoritative patterns) for policy export. pub async fn fetch_blessed_assertions(&self) -> Result, AphoriaError> { self.fetch_assertions_by_predicate(predicates::BLESSED).await } /// Fetch all authoritative assertions imported from Trust Packs. /// /// These are assertions imported via `policy import` that should be used /// for conflict detection during scans. They are indexed under the /// "authoritative" predicate key. pub async fn fetch_authoritative_assertions(&self) -> Result, AphoriaError> { self.fetch_assertions_by_predicate(predicates::AUTHORITATIVE).await } /// Fetch assertions by predicate from the predicate index. async fn fetch_assertions_by_predicate( &self, predicate: &str, ) -> Result, AphoriaError> { let hashes = self.predicate_index_store.get_by_predicate(predicate).await.map_err(|e| { AphoriaError::Storage(format!( "Failed to fetch predicate index for '{}': {e}", predicate )) })?; let mut assertions = Vec::new(); for hash in hashes { if let Some(assertion) = self.load_assertion_by_hash(&hash).await { assertions.push(assertion); } } info!(predicate, count = assertions.len(), "Fetched assertions by predicate"); Ok(assertions) } }