use crate::governance::{
EnergySnapshot, GateDecision, Hash, PolicyBundleRef, Timestamp, WitnessId, WitnessRecord,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use uuid::Uuid;
#[cfg(feature = "ruvllm")]
pub use ruvllm::witness_log::{
LatencyBreakdown, RoutingDecision, WitnessEntry as InferenceWitness, WitnessLogStats,
};
#[derive(Debug, Error)]
pub enum UnifiedWitnessError {
#[error("Witness not found: {0}")]
NotFound(String),
#[error("Chain integrity violation at witness {witness_id}: {reason}")]
ChainViolation { witness_id: String, reason: String },
#[error("Hash mismatch: expected {expected}, got {actual}")]
HashMismatch { expected: String, actual: String },
#[error("Invalid witness data: {0}")]
InvalidData(String),
#[error("Storage error: {0}")]
Storage(String),
#[error("Session not found: {0}")]
SessionNotFound(String),
#[error("Governance witness error: {0}")]
GovernanceError(String),
#[error("Inference witness error: {0}")]
InferenceError(String),
}
pub type Result<T> = std::result::Result<T, UnifiedWitnessError>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GenerationWitnessId(pub Uuid);
impl GenerationWitnessId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
#[must_use]
pub const fn nil() -> Self {
Self(Uuid::nil())
}
#[must_use]
pub fn is_nil(&self) -> bool {
self.0.is_nil()
}
}
impl Default for GenerationWitnessId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for GenerationWitnessId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InferenceWitnessSummary {
pub request_id: Uuid,
pub session_id: String,
pub model_used: String,
pub quality_score: f32,
pub routing_confidence: f32,
pub total_latency_ms: f32,
pub is_success: bool,
pub error_message: Option<String>,
pub timestamp: Timestamp,
pub query_embedding: Option<Vec<f32>>,
pub content_hash: Hash,
}
impl InferenceWitnessSummary {
#[cfg(feature = "ruvllm")]
pub fn from_inference_witness(witness: &InferenceWitness) -> Self {
Self {
request_id: witness.request_id,
session_id: witness.session_id.clone(),
model_used: format!("{:?}", witness.model_used),
quality_score: witness.quality_score,
routing_confidence: witness.routing_decision.confidence,
total_latency_ms: witness.latency.total_ms,
is_success: witness.is_success(),
error_message: witness.error.as_ref().map(|e| format!("{:?}", e)),
timestamp: Timestamp::from(witness.timestamp),
query_embedding: Some(witness.query_embedding.clone()),
content_hash: Self::compute_hash(witness),
}
}
#[cfg(feature = "ruvllm")]
fn compute_hash(witness: &InferenceWitness) -> Hash {
let mut hasher = blake3::Hasher::new();
hasher.update(witness.request_id.as_bytes());
hasher.update(witness.session_id.as_bytes());
hasher.update(&witness.quality_score.to_le_bytes());
hasher.update(&witness.latency.total_ms.to_le_bytes());
hasher.update(&[witness.is_success() as u8]);
Hash::from_blake3(hasher.finalize())
}
pub fn minimal(
request_id: Uuid,
session_id: String,
model_used: String,
quality_score: f32,
) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(request_id.as_bytes());
hasher.update(session_id.as_bytes());
hasher.update(&quality_score.to_le_bytes());
let content_hash = Hash::from_blake3(hasher.finalize());
Self {
request_id,
session_id,
model_used,
quality_score,
routing_confidence: 1.0,
total_latency_ms: 0.0,
is_success: true,
error_message: None,
timestamp: Timestamp::now(),
query_embedding: None,
content_hash,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CoherenceWitnessSummary {
pub witness_id: WitnessId,
pub decision: GateDecision,
pub energy_snapshot: EnergySnapshot,
pub policy_bundle_ref: PolicyBundleRef,
pub timestamp: Timestamp,
pub content_hash: Hash,
}
impl CoherenceWitnessSummary {
pub fn from_witness_record(record: &WitnessRecord) -> Self {
Self {
witness_id: record.id,
decision: record.decision.clone(),
energy_snapshot: record.energy_snapshot.clone(),
policy_bundle_ref: record.policy_bundle_ref.clone(),
timestamp: record.timestamp,
content_hash: record.content_hash,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GenerationWitness {
pub id: GenerationWitnessId,
pub sequence: u64,
pub inference: InferenceWitnessSummary,
pub coherence: CoherenceWitnessSummary,
pub combined_hash: Hash,
pub previous_witness: Option<GenerationWitnessId>,
pub previous_hash: Option<Hash>,
pub content_hash: Hash,
pub actor: Option<String>,
pub correlation_id: Option<String>,
pub tags: Vec<String>,
pub created_at: Timestamp,
}
impl GenerationWitness {
pub fn new(
inference: InferenceWitnessSummary,
coherence: CoherenceWitnessSummary,
previous: Option<&GenerationWitness>,
) -> Self {
let id = GenerationWitnessId::new();
let created_at = Timestamp::now();
let combined_hash = Self::compute_combined_hash(&inference, &coherence);
let (previous_witness, previous_hash, sequence) = match previous {
Some(prev) => (Some(prev.id), Some(prev.content_hash), prev.sequence + 1),
None => (None, None, 0),
};
let mut witness = Self {
id,
sequence,
inference,
coherence,
combined_hash,
previous_witness,
previous_hash,
content_hash: Hash::zero(), actor: None,
correlation_id: None,
tags: Vec::new(),
created_at,
};
witness.content_hash = witness.compute_content_hash();
witness
}
pub fn genesis(
inference: InferenceWitnessSummary,
coherence: CoherenceWitnessSummary,
) -> Self {
Self::new(inference, coherence, None)
}
#[must_use]
pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
self.actor = Some(actor.into());
self.content_hash = self.compute_content_hash();
self
}
#[must_use]
pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
self.correlation_id = Some(id.into());
self.content_hash = self.compute_content_hash();
self
}
#[must_use]
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self.content_hash = self.compute_content_hash();
self
}
fn compute_combined_hash(
inference: &InferenceWitnessSummary,
coherence: &CoherenceWitnessSummary,
) -> Hash {
let mut hasher = blake3::Hasher::new();
hasher.update(inference.content_hash.as_bytes());
hasher.update(coherence.content_hash.as_bytes());
Hash::from_blake3(hasher.finalize())
}
pub fn compute_content_hash(&self) -> Hash {
let mut hasher = blake3::Hasher::new();
hasher.update(self.id.as_bytes());
hasher.update(&self.sequence.to_le_bytes());
hasher.update(self.combined_hash.as_bytes());
if let Some(ref prev_id) = self.previous_witness {
hasher.update(prev_id.as_bytes());
}
if let Some(ref prev_hash) = self.previous_hash {
hasher.update(prev_hash.as_bytes());
}
if let Some(ref actor) = self.actor {
hasher.update(actor.as_bytes());
}
if let Some(ref corr_id) = self.correlation_id {
hasher.update(corr_id.as_bytes());
}
for tag in &self.tags {
hasher.update(tag.as_bytes());
}
hasher.update(&self.created_at.secs.to_le_bytes());
hasher.update(&self.created_at.nanos.to_le_bytes());
Hash::from_blake3(hasher.finalize())
}
#[must_use]
pub fn verify_content_hash(&self) -> bool {
self.content_hash == self.compute_content_hash()
}
pub fn verify_chain_link(&self, previous: &GenerationWitness) -> Result<()> {
if self.previous_witness != Some(previous.id) {
return Err(UnifiedWitnessError::ChainViolation {
witness_id: self.id.to_string(),
reason: format!(
"Previous witness ID mismatch: expected {:?}, got {:?}",
Some(previous.id),
self.previous_witness
),
});
}
if self.previous_hash != Some(previous.content_hash) {
return Err(UnifiedWitnessError::HashMismatch {
expected: previous.content_hash.to_hex(),
actual: self
.previous_hash
.map(|h| h.to_hex())
.unwrap_or_else(|| "None".to_string()),
});
}
if self.sequence != previous.sequence + 1 {
return Err(UnifiedWitnessError::ChainViolation {
witness_id: self.id.to_string(),
reason: format!(
"Sequence discontinuity: expected {}, got {}",
previous.sequence + 1,
self.sequence
),
});
}
Ok(())
}
#[must_use]
pub fn is_genesis(&self) -> bool {
self.previous_witness.is_none() && self.sequence == 0
}
#[must_use]
pub fn session_id(&self) -> &str {
&self.inference.session_id
}
#[must_use]
pub fn was_allowed(&self) -> bool {
self.coherence.decision.allow
}
#[must_use]
pub fn was_successful(&self) -> bool {
self.inference.is_success
}
#[must_use]
pub fn quality_score(&self) -> f32 {
self.inference.quality_score
}
#[must_use]
pub fn coherence_energy(&self) -> f32 {
self.coherence.energy_snapshot.total_energy
}
}
impl PartialEq for GenerationWitness {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for GenerationWitness {}
impl std::hash::Hash for GenerationWitness {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct UnifiedWitnessStats {
pub total_witnesses: usize,
pub sessions: usize,
pub allowed_count: usize,
pub denied_count: usize,
pub success_count: usize,
pub error_count: usize,
pub avg_quality_score: f32,
pub avg_coherence_energy: f32,
pub chain_verified: bool,
}
#[derive(Clone, Debug, Default)]
pub struct WitnessQuery {
pub session_id: Option<String>,
pub actor: Option<String>,
pub tags: Option<Vec<String>>,
pub allowed: Option<bool>,
pub successful: Option<bool>,
pub min_quality: Option<f32>,
pub max_energy: Option<f32>,
pub start_time: Option<Timestamp>,
pub end_time: Option<Timestamp>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
impl WitnessQuery {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn session(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
#[must_use]
pub fn actor(mut self, actor: impl Into<String>) -> Self {
self.actor = Some(actor.into());
self
}
#[must_use]
pub fn allowed(mut self, allowed: bool) -> Self {
self.allowed = Some(allowed);
self
}
#[must_use]
pub fn successful(mut self, successful: bool) -> Self {
self.successful = Some(successful);
self
}
#[must_use]
pub fn min_quality(mut self, score: f32) -> Self {
self.min_quality = Some(score);
self
}
#[must_use]
pub fn max_energy(mut self, energy: f32) -> Self {
self.max_energy = Some(energy);
self
}
#[must_use]
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Debug)]
pub struct UnifiedWitnessLog {
witnesses: Vec<GenerationWitness>,
by_id: HashMap<GenerationWitnessId, usize>,
by_session: HashMap<String, Vec<usize>>,
by_correlation: HashMap<String, Vec<usize>>,
head: Option<GenerationWitnessId>,
chain_verified: bool,
}
impl UnifiedWitnessLog {
pub fn new() -> Self {
Self {
witnesses: Vec::new(),
by_id: HashMap::new(),
by_session: HashMap::new(),
by_correlation: HashMap::new(),
head: None,
chain_verified: true,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
witnesses: Vec::with_capacity(capacity),
by_id: HashMap::with_capacity(capacity),
by_session: HashMap::new(),
by_correlation: HashMap::new(),
head: None,
chain_verified: true,
}
}
pub fn record_generation(
&mut self,
inference: InferenceWitnessSummary,
coherence: CoherenceWitnessSummary,
) -> Result<&GenerationWitness> {
let previous = self.head.and_then(|id| self.get(&id));
let witness = GenerationWitness::new(inference, coherence, previous);
self.insert(witness)
}
#[cfg(feature = "ruvllm")]
pub fn record_generation_full(
&mut self,
inference: &InferenceWitness,
coherence: &WitnessRecord,
) -> Result<&GenerationWitness> {
let inference_summary = InferenceWitnessSummary::from_inference_witness(inference);
let coherence_summary = CoherenceWitnessSummary::from_witness_record(coherence);
self.record_generation(inference_summary, coherence_summary)
}
fn insert(&mut self, witness: GenerationWitness) -> Result<&GenerationWitness> {
let id = witness.id;
let session_id = witness.session_id().to_string();
let correlation_id = witness.correlation_id.clone();
let index = self.witnesses.len();
self.by_id.insert(id, index);
self.by_session
.entry(session_id)
.or_default()
.push(index);
if let Some(corr_id) = correlation_id {
self.by_correlation
.entry(corr_id)
.or_default()
.push(index);
}
self.head = Some(id);
self.witnesses.push(witness);
Ok(&self.witnesses[index])
}
pub fn get(&self, id: &GenerationWitnessId) -> Option<&GenerationWitness> {
self.by_id.get(id).map(|&idx| &self.witnesses[idx])
}
pub fn head(&self) -> Option<&GenerationWitness> {
self.head.and_then(|id| self.get(&id))
}
pub fn query_by_session(&self, session_id: &str) -> Vec<&GenerationWitness> {
self.by_session
.get(session_id)
.map(|indices| indices.iter().map(|&idx| &self.witnesses[idx]).collect())
.unwrap_or_default()
}
pub fn query_by_correlation(&self, correlation_id: &str) -> Vec<&GenerationWitness> {
self.by_correlation
.get(correlation_id)
.map(|indices| indices.iter().map(|&idx| &self.witnesses[idx]).collect())
.unwrap_or_default()
}
pub fn query(&self, query: &WitnessQuery) -> Vec<&GenerationWitness> {
let mut results: Vec<&GenerationWitness> = self.witnesses.iter().collect();
if let Some(ref session) = query.session_id {
results.retain(|w| w.session_id() == session);
}
if let Some(ref actor) = query.actor {
results.retain(|w| w.actor.as_deref() == Some(actor.as_str()));
}
if let Some(allowed) = query.allowed {
results.retain(|w| w.was_allowed() == allowed);
}
if let Some(successful) = query.successful {
results.retain(|w| w.was_successful() == successful);
}
if let Some(min_quality) = query.min_quality {
results.retain(|w| w.quality_score() >= min_quality);
}
if let Some(max_energy) = query.max_energy {
results.retain(|w| w.coherence_energy() <= max_energy);
}
if let Some(ref start) = query.start_time {
results.retain(|w| w.created_at >= *start);
}
if let Some(ref end) = query.end_time {
results.retain(|w| w.created_at <= *end);
}
if let Some(ref tags) = query.tags {
results.retain(|w| w.tags.iter().any(|t| tags.contains(t)));
}
if let Some(offset) = query.offset {
results = results.into_iter().skip(offset).collect();
}
if let Some(limit) = query.limit {
results.truncate(limit);
}
results
}
pub fn sessions(&self) -> Vec<&str> {
self.by_session.keys().map(|s| s.as_str()).collect()
}
pub fn len(&self) -> usize {
self.witnesses.len()
}
pub fn is_empty(&self) -> bool {
self.witnesses.is_empty()
}
pub fn verify_chain(&mut self) -> Result<bool> {
if self.witnesses.is_empty() {
self.chain_verified = true;
return Ok(true);
}
if !self.witnesses[0].is_genesis() {
self.chain_verified = false;
return Err(UnifiedWitnessError::ChainViolation {
witness_id: self.witnesses[0].id.to_string(),
reason: "First witness is not genesis".to_string(),
});
}
for witness in &self.witnesses {
if !witness.verify_content_hash() {
self.chain_verified = false;
return Err(UnifiedWitnessError::HashMismatch {
expected: witness.content_hash.to_hex(),
actual: witness.compute_content_hash().to_hex(),
});
}
}
for i in 1..self.witnesses.len() {
self.witnesses[i].verify_chain_link(&self.witnesses[i - 1])?;
}
self.chain_verified = true;
Ok(true)
}
pub fn stats(&self) -> UnifiedWitnessStats {
if self.witnesses.is_empty() {
return UnifiedWitnessStats::default();
}
let allowed_count = self.witnesses.iter().filter(|w| w.was_allowed()).count();
let success_count = self.witnesses.iter().filter(|w| w.was_successful()).count();
let total_quality: f32 = self.witnesses.iter().map(|w| w.quality_score()).sum();
let total_energy: f32 = self.witnesses.iter().map(|w| w.coherence_energy()).sum();
UnifiedWitnessStats {
total_witnesses: self.witnesses.len(),
sessions: self.by_session.len(),
allowed_count,
denied_count: self.witnesses.len() - allowed_count,
success_count,
error_count: self.witnesses.len() - success_count,
avg_quality_score: total_quality / self.witnesses.len() as f32,
avg_coherence_energy: total_energy / self.witnesses.len() as f32,
chain_verified: self.chain_verified,
}
}
pub fn export_session(&self, session_id: &str) -> Result<String> {
let witnesses = self.query_by_session(session_id);
if witnesses.is_empty() {
return Err(UnifiedWitnessError::SessionNotFound(session_id.to_string()));
}
serde_json::to_string_pretty(&witnesses)
.map_err(|e| UnifiedWitnessError::Storage(e.to_string()))
}
pub fn range_by_sequence(&self, start: u64, end: u64) -> Vec<&GenerationWitness> {
self.witnesses
.iter()
.filter(|w| w.sequence >= start && w.sequence <= end)
.collect()
}
pub fn low_quality_witnesses(&self, threshold: f32) -> Vec<&GenerationWitness> {
self.witnesses
.iter()
.filter(|w| w.quality_score() < threshold)
.collect()
}
pub fn high_energy_witnesses(&self, threshold: f32) -> Vec<&GenerationWitness> {
self.witnesses
.iter()
.filter(|w| w.coherence_energy() > threshold)
.collect()
}
pub fn denied_generations(&self) -> Vec<&GenerationWitness> {
self.witnesses.iter().filter(|w| !w.was_allowed()).collect()
}
pub fn failed_inferences(&self) -> Vec<&GenerationWitness> {
self.witnesses
.iter()
.filter(|w| !w.was_successful())
.collect()
}
}
impl Default for UnifiedWitnessLog {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::governance::{
EnergySnapshot, GateDecision, Hash, PolicyBundleId, PolicyBundleRef, Timestamp, Version,
WitnessComputeLane as ComputeLane, WitnessId,
};
fn test_inference_summary() -> InferenceWitnessSummary {
InferenceWitnessSummary::minimal(
Uuid::new_v4(),
"test-session".to_string(),
"small".to_string(),
0.85,
)
}
fn test_coherence_summary() -> CoherenceWitnessSummary {
CoherenceWitnessSummary {
witness_id: WitnessId::new(),
decision: GateDecision::allow(ComputeLane::Reflex),
energy_snapshot: EnergySnapshot::new(0.3, 0.2, "test-scope"),
policy_bundle_ref: PolicyBundleRef {
id: PolicyBundleId::new(),
version: Version::initial(),
content_hash: Hash::zero(),
},
timestamp: Timestamp::now(),
content_hash: Hash::zero(),
}
}
#[test]
fn test_generation_witness_creation() {
let inference = test_inference_summary();
let coherence = test_coherence_summary();
let witness = GenerationWitness::genesis(inference, coherence);
assert!(witness.is_genesis());
assert!(witness.verify_content_hash());
assert!(witness.was_allowed());
assert!(witness.was_successful());
assert!((witness.quality_score() - 0.85).abs() < f32::EPSILON);
}
#[test]
fn test_witness_chain() {
let mut log = UnifiedWitnessLog::new();
let w1 = log
.record_generation(test_inference_summary(), test_coherence_summary())
.unwrap();
assert!(w1.is_genesis());
assert_eq!(w1.sequence, 0);
let w2 = log
.record_generation(test_inference_summary(), test_coherence_summary())
.unwrap();
assert!(!w2.is_genesis());
assert_eq!(w2.sequence, 1);
assert!(w2.previous_witness.is_some());
assert!(log.verify_chain().unwrap());
}
#[test]
fn test_session_query() {
let mut log = UnifiedWitnessLog::new();
let mut inference1 = test_inference_summary();
inference1.session_id = "session-1".to_string();
log.record_generation(inference1, test_coherence_summary())
.unwrap();
let mut inference2 = test_inference_summary();
inference2.session_id = "session-2".to_string();
log.record_generation(inference2, test_coherence_summary())
.unwrap();
let mut inference3 = test_inference_summary();
inference3.session_id = "session-1".to_string();
log.record_generation(inference3, test_coherence_summary())
.unwrap();
let session1_witnesses = log.query_by_session("session-1");
assert_eq!(session1_witnesses.len(), 2);
let session2_witnesses = log.query_by_session("session-2");
assert_eq!(session2_witnesses.len(), 1);
}
#[test]
fn test_witness_query_filters() {
let mut log = UnifiedWitnessLog::new();
let mut inference_high = test_inference_summary();
inference_high.quality_score = 0.95;
log.record_generation(inference_high, test_coherence_summary())
.unwrap();
let mut inference_low = test_inference_summary();
inference_low.quality_score = 0.3;
log.record_generation(inference_low, test_coherence_summary())
.unwrap();
let high_quality = log.query(&WitnessQuery::new().min_quality(0.8));
assert_eq!(high_quality.len(), 1);
assert!((high_quality[0].quality_score() - 0.95).abs() < f32::EPSILON);
}
#[test]
fn test_stats() {
let mut log = UnifiedWitnessLog::new();
log.record_generation(test_inference_summary(), test_coherence_summary())
.unwrap();
log.record_generation(test_inference_summary(), test_coherence_summary())
.unwrap();
let stats = log.stats();
assert_eq!(stats.total_witnesses, 2);
assert_eq!(stats.allowed_count, 2);
assert_eq!(stats.success_count, 2);
assert!(stats.chain_verified);
}
#[test]
fn test_tamper_detection() {
let inference = test_inference_summary();
let coherence = test_coherence_summary();
let mut witness = GenerationWitness::genesis(inference, coherence);
assert!(witness.verify_content_hash());
witness.inference.quality_score = 0.99;
assert!(!witness.verify_content_hash());
}
#[test]
fn test_chain_verification() {
let inference = test_inference_summary();
let coherence = test_coherence_summary();
let genesis = GenerationWitness::genesis(inference.clone(), coherence.clone());
let second = GenerationWitness::new(inference.clone(), coherence.clone(), Some(&genesis));
assert!(second.verify_chain_link(&genesis).is_ok());
let mut bad_witness = GenerationWitness::new(inference, coherence, Some(&genesis));
bad_witness.previous_witness = Some(GenerationWitnessId::new());
assert!(bad_witness.verify_chain_link(&genesis).is_err());
}
#[test]
fn test_denied_and_failed_queries() {
let mut log = UnifiedWitnessLog::new();
log.record_generation(test_inference_summary(), test_coherence_summary())
.unwrap();
let mut denied_coherence = test_coherence_summary();
denied_coherence.decision = GateDecision::deny(ComputeLane::Heavy, "High energy");
log.record_generation(test_inference_summary(), denied_coherence)
.unwrap();
let mut failed_inference = test_inference_summary();
failed_inference.is_success = false;
failed_inference.error_message = Some("Timeout".to_string());
log.record_generation(failed_inference, test_coherence_summary())
.unwrap();
let denied = log.denied_generations();
assert_eq!(denied.len(), 1);
let failed = log.failed_inferences();
assert_eq!(failed.len(), 1);
}
}