//! Ephemeral conflict detector that works entirely in-memory. //! //! This is the fast path for `aphoria scan` when persistence is not needed. //! It builds the authoritative corpus and concept index once, then can check //! conflicts against any number of claims without disk I/O. use std::collections::HashMap; use ed25519_dalek::SigningKey; use stemedb_core::types::Assertion; use tracing::{info, instrument, warn}; use crate::config::{AphoriaConfig, CorpusConfig}; use crate::corpus::CorpusRegistry; use crate::policy::TrustPack; use crate::types::{ConflictResult, Observation, PolicySourceInfo, PredicateAliasSet}; use super::concept_index::ConceptIndex; use super::conflict::check_conflicts_with_predicate_aliases; use super::corpus::current_timestamp; /// Ephemeral conflict detector that works entirely in-memory. /// /// This is the fast path for `aphoria scan` when persistence is not needed. /// It builds the authoritative corpus and concept index once, then can check /// conflicts against any number of claims without disk I/O. /// /// # Example /// /// ```ignore /// let detector = EphemeralDetector::new(&signing_key, &corpus_config); /// let conflicts = detector.check_conflicts(&claims, &config); /// ``` pub struct EphemeralDetector { /// Pre-built authoritative corpus (RFC, OWASP, vendor assertions). #[allow(dead_code)] corpus: Vec, /// In-memory index for tail-path matching. index: ConceptIndex, /// In-memory aliases from policies. aliases: HashMap, /// Mapping from assertion subject to policy source info. /// Used to track which Trust Pack an assertion came from. pack_sources: HashMap, /// Predicate aliases for semantic matching. predicate_aliases: Vec, } impl EphemeralDetector { /// Create a new ephemeral detector with the full authoritative corpus. /// /// This builds the corpus from all configured sources (hardcoded, RFC, OWASP, vendor) /// using the CorpusRegistry. The corpus and index are built entirely in-memory. /// /// # Arguments /// /// * `signing_key` - Ed25519 key for signing assertions /// * `corpus_config` - Configuration for corpus sources #[instrument(skip(signing_key, corpus_config))] pub async fn new(signing_key: &SigningKey, corpus_config: &CorpusConfig) -> Self { let registry = CorpusRegistry::with_defaults(corpus_config); let timestamp = current_timestamp(); // Build the full corpus from registry (offline mode to avoid network I/O) let result = registry.build_all(signing_key, timestamp, corpus_config, true).await; let corpus = match result { Ok(build_result) => { info!( total = build_result.total_assertions(), successful = build_result.successful_builders(), skipped = build_result.skipped_builders(), "Corpus built from registry" ); build_result.assertions } Err(e) => { warn!(error = %e, "Corpus build failed, using empty corpus"); Vec::new() } }; let index = ConceptIndex::build(&corpus); info!( corpus_size = corpus.len(), index_entries = index.entries.len(), "EphemeralDetector initialized" ); Self { corpus, index, aliases: HashMap::new(), pack_sources: HashMap::new(), predicate_aliases: Vec::new(), } } /// Create a new ephemeral detector with just the hardcoded corpus. /// /// This is a faster initialization path that only uses the built-in assertions. /// Useful for testing or when minimal corpus is sufficient. #[allow(dead_code)] #[instrument(skip(signing_key))] pub async fn new_minimal(signing_key: &SigningKey) -> Self { let corpus = super::create_authoritative_corpus(signing_key).await; let index = ConceptIndex::build(&corpus); info!( corpus_size = corpus.len(), index_entries = index.entries.len(), "EphemeralDetector initialized (minimal corpus)" ); Self { corpus, index, aliases: HashMap::new(), pack_sources: HashMap::new(), predicate_aliases: Vec::new(), } } /// Ingest policies into the detector. /// /// Adds assertions from trust packs to the corpus/index and aliases to the alias map. /// Also tracks which pack each assertion came from for provenance reporting, /// and imports predicate aliases for semantic matching. pub fn ingest_policies(&mut self, policies: &[TrustPack]) { let mut new_assertions = 0; let mut new_aliases = 0; let mut new_predicate_aliases = 0; for pack in policies { // Create policy source info for this pack let policy_info = PolicySourceInfo { pack_name: pack.header.name.clone(), pack_version: pack.header.version.clone(), issuer_hex: hex::encode(&pack.header.issuer_id[..4]), signer_name: pack.header.signer_name.clone(), contact: pack.header.contact.clone(), }; // Add assertions to corpus and index // Use predicate alias normalization when building keys for assertion in &pack.assertions { self.corpus.push(assertion.clone()); // Normalize predicate using current predicate aliases let normalized_predicate = ConceptIndex::normalize_predicate( &assertion.predicate, &self.predicate_aliases, ); // Add to index with normalized predicate if let Some(key) = ConceptIndex::make_key(&assertion.subject, normalized_predicate) { self.index.entries.entry(key).or_default().push(assertion.clone()); } // Track pack source for this assertion (keyed by subject) self.pack_sources.insert(assertion.subject.clone(), policy_info.clone()); new_assertions += 1; } // Add concept aliases for alias in &pack.aliases { self.aliases.insert(alias.alias.to_string(), alias.canonical.to_string()); new_aliases += 1; } // Add predicate aliases from pack for pack_alias in &pack.predicate_aliases { self.predicate_aliases.push(PredicateAliasSet::from(pack_alias)); new_predicate_aliases += 1; } } info!(new_assertions, new_aliases, new_predicate_aliases, "Ingested policies"); } /// Set predicate aliases from config. /// /// This allows predicate aliases to be configured in aphoria.toml /// in addition to or instead of importing them from Trust Packs. #[allow(dead_code)] pub fn set_predicate_aliases(&mut self, aliases: Vec) { self.predicate_aliases = aliases; } /// Get the current predicate aliases. #[allow(dead_code)] pub fn predicate_aliases(&self) -> &[PredicateAliasSet] { &self.predicate_aliases } /// Get the policy source info for a given assertion subject. #[allow(dead_code)] // Used in tests pub fn get_pack_source(&self, subject: &str) -> Option<&PolicySourceInfo> { self.pack_sources.get(subject) } /// Check for conflicts between extracted claims and authoritative sources. /// /// This is a pure in-memory operation. No persistence, no aliases created. /// Uses both predicate aliases from config and those imported from Trust Packs. /// /// # Arguments /// /// * `claims` - Extracted claims from source code /// * `config` - Configuration with thresholds /// /// # Returns /// /// Vector of conflict results, with debug traces populated based on config. pub fn check_conflicts( &self, claims: &[Observation], config: &AphoriaConfig, ) -> Vec { // Merge predicate aliases from config and from imported packs let mut all_aliases = config.predicate_aliases.to_alias_sets(); all_aliases.extend(self.predicate_aliases.clone()); check_conflicts_with_predicate_aliases( claims, &self.index, &self.aliases, &self.pack_sources, &all_aliases, config, false, ) } /// Check for conflicts with debug traces enabled. /// /// Like `check_conflicts`, but populates `ConflictTrace` for each result. pub fn check_conflicts_debug( &self, claims: &[Observation], config: &AphoriaConfig, ) -> Vec { // Merge predicate aliases from config and from imported packs let mut all_aliases = config.predicate_aliases.to_alias_sets(); all_aliases.extend(self.predicate_aliases.clone()); check_conflicts_with_predicate_aliases( claims, &self.index, &self.aliases, &self.pack_sources, &all_aliases, config, true, ) } /// Get the number of authoritative assertions in the corpus. #[allow(dead_code)] pub fn corpus_size(&self) -> usize { self.corpus.len() } /// Get the number of indexed concept keys. #[allow(dead_code)] pub fn index_size(&self) -> usize { self.index.entries.len() } }