use crate::coherence::CoherenceEngine;
use crate::error::CoherenceError;
use crate::execution::{
Action, ActionImpact, ActionMetadata, CoherenceGate, EnergySnapshot, ExecutionContext,
GateDecision, ScopeId as ExecScopeId, WitnessRecord as ExecWitnessRecord,
};
use crate::governance::{Hash, PolicyBundleRef, Timestamp, WitnessRecord as GovWitnessRecord};
use crate::substrate::{
RestrictionMap, SheafEdge, SheafEdgeBuilder, SheafGraph, SheafNode, SheafNodeBuilder,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationContext {
pub context_embedding: Vec<f32>,
pub response_embedding: Vec<f32>,
pub supporting_embeddings: Vec<Vec<f32>>,
pub scope: String,
pub edge_weights: EdgeWeights,
pub metadata: HashMap<String, String>,
pub request_id: Uuid,
}
impl ValidationContext {
pub fn new() -> Self {
Self {
context_embedding: Vec::new(),
response_embedding: Vec::new(),
supporting_embeddings: Vec::new(),
scope: "default".to_string(),
edge_weights: EdgeWeights::default(),
metadata: HashMap::new(),
request_id: Uuid::new_v4(),
}
}
pub fn with_context_embedding(mut self, embedding: Vec<f32>) -> Self {
self.context_embedding = embedding;
self
}
pub fn with_response_embedding(mut self, embedding: Vec<f32>) -> Self {
self.response_embedding = embedding;
self
}
pub fn with_supporting_embedding(mut self, embedding: Vec<f32>) -> Self {
self.supporting_embeddings.push(embedding);
self
}
pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
self.scope = scope.into();
self
}
pub fn with_edge_weights(mut self, weights: EdgeWeights) -> Self {
self.edge_weights = weights;
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_request_id(mut self, id: Uuid) -> Self {
self.request_id = id;
self
}
pub fn embedding_dim(&self) -> usize {
if !self.context_embedding.is_empty() {
self.context_embedding.len()
} else if !self.response_embedding.is_empty() {
self.response_embedding.len()
} else {
0
}
}
pub fn validate(&self) -> Result<(), ValidationError> {
if self.context_embedding.is_empty() {
return Err(ValidationError::MissingEmbedding("context".to_string()));
}
if self.response_embedding.is_empty() {
return Err(ValidationError::MissingEmbedding("response".to_string()));
}
if self.context_embedding.len() != self.response_embedding.len() {
return Err(ValidationError::DimensionMismatch {
context_dim: self.context_embedding.len(),
response_dim: self.response_embedding.len(),
});
}
for emb in &self.supporting_embeddings {
if emb.len() != self.context_embedding.len() {
return Err(ValidationError::DimensionMismatch {
context_dim: self.context_embedding.len(),
response_dim: emb.len(),
});
}
}
Ok(())
}
}
impl Default for ValidationContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeWeights {
pub context_response: f32,
pub response_support: f32,
pub context_support: f32,
pub support_support: f32,
}
impl EdgeWeights {
pub fn new(
context_response: f32,
response_support: f32,
context_support: f32,
support_support: f32,
) -> Self {
Self {
context_response,
response_support,
context_support,
support_support,
}
}
pub fn strict() -> Self {
Self {
context_response: 2.0,
response_support: 1.5,
context_support: 1.0,
support_support: 0.5,
}
}
pub fn permissive() -> Self {
Self {
context_response: 1.0,
response_support: 0.5,
context_support: 0.3,
support_support: 0.2,
}
}
}
impl Default for EdgeWeights {
fn default() -> Self {
Self {
context_response: 1.5,
response_support: 1.0,
context_support: 0.8,
support_support: 0.3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub allowed: bool,
pub energy: f32,
pub reason: Option<String>,
pub witness: ValidationWitness,
pub edge_breakdown: HashMap<String, f32>,
pub timestamp: Timestamp,
pub request_id: Uuid,
}
impl ValidationResult {
pub fn allow(energy: f32, witness: ValidationWitness, request_id: Uuid) -> Self {
Self {
allowed: true,
energy,
reason: None,
witness,
edge_breakdown: HashMap::new(),
timestamp: Timestamp::now(),
request_id,
}
}
pub fn deny(
energy: f32,
reason: impl Into<String>,
witness: ValidationWitness,
request_id: Uuid,
) -> Self {
Self {
allowed: false,
energy,
reason: Some(reason.into()),
witness,
edge_breakdown: HashMap::new(),
timestamp: Timestamp::now(),
request_id,
}
}
pub fn with_edge_breakdown(mut self, breakdown: HashMap<String, f32>) -> Self {
self.edge_breakdown = breakdown;
self
}
pub fn is_coherent(&self, threshold: f32) -> bool {
self.energy < threshold
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationWitness {
pub id: Uuid,
pub context_hash: Hash,
pub response_hash: Hash,
pub energy: f32,
pub scope: String,
pub decision: WitnessDecision,
pub policy_ref: Option<PolicyBundleRef>,
pub timestamp: Timestamp,
pub fingerprint: Hash,
}
impl ValidationWitness {
pub fn new(
context: &ValidationContext,
energy: f32,
decision: WitnessDecision,
policy_ref: Option<PolicyBundleRef>,
) -> Self {
let context_hash = Self::compute_embedding_hash(&context.context_embedding);
let response_hash = Self::compute_embedding_hash(&context.response_embedding);
let mut witness = Self {
id: Uuid::new_v4(),
context_hash,
response_hash,
energy,
scope: context.scope.clone(),
decision,
policy_ref,
timestamp: Timestamp::now(),
fingerprint: Hash::zero(),
};
witness.fingerprint = witness.compute_fingerprint();
witness
}
fn compute_embedding_hash(embedding: &[f32]) -> Hash {
let mut hasher = blake3::Hasher::new();
for &val in embedding {
hasher.update(&val.to_le_bytes());
}
Hash::from_blake3(hasher.finalize())
}
fn compute_fingerprint(&self) -> Hash {
let mut hasher = blake3::Hasher::new();
hasher.update(self.id.as_bytes());
hasher.update(self.context_hash.as_bytes());
hasher.update(self.response_hash.as_bytes());
hasher.update(&self.energy.to_le_bytes());
hasher.update(self.scope.as_bytes());
hasher.update(&[self.decision.allowed as u8]);
hasher.update(&self.timestamp.secs.to_le_bytes());
hasher.update(&self.timestamp.nanos.to_le_bytes());
Hash::from_blake3(hasher.finalize())
}
pub fn verify_integrity(&self) -> bool {
self.fingerprint == self.compute_fingerprint()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WitnessDecision {
pub allowed: bool,
pub lane: u8,
pub reason: Option<String>,
pub confidence: f32,
}
impl WitnessDecision {
pub fn allow(lane: u8, confidence: f32) -> Self {
Self {
allowed: true,
lane,
reason: None,
confidence,
}
}
pub fn deny(lane: u8, reason: impl Into<String>, confidence: f32) -> Self {
Self {
allowed: false,
lane,
reason: Some(reason.into()),
confidence,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Missing embedding: {0}")]
MissingEmbedding(String),
#[error("Dimension mismatch: context={context_dim}, response={response_dim}")]
DimensionMismatch {
context_dim: usize,
response_dim: usize,
},
#[error("Coherence computation failed: {0}")]
CoherenceError(#[from] CoherenceError),
#[error("Graph construction failed: {0}")]
GraphError(String),
#[error("Policy not found for scope: {0}")]
PolicyNotFound(String),
#[error("Internal error: {0}")]
Internal(String),
}
struct ValidationAction {
scope: ExecScopeId,
impact: ActionImpact,
metadata: ActionMetadata,
content_hash: [u8; 32],
}
impl ValidationAction {
fn new(context: &ValidationContext) -> Self {
let mut hasher = blake3::Hasher::new();
for &val in &context.context_embedding {
hasher.update(&val.to_le_bytes());
}
for &val in &context.response_embedding {
hasher.update(&val.to_le_bytes());
}
let hash = hasher.finalize();
let mut content_hash = [0u8; 32];
content_hash.copy_from_slice(hash.as_bytes());
Self {
scope: ExecScopeId::new(&context.scope),
impact: ActionImpact::medium(),
metadata: ActionMetadata::new(
"LLMValidation",
"Coherence validation for LLM response",
&context.request_id.to_string(),
),
content_hash,
}
}
}
impl Action for ValidationAction {
type Output = ();
type Error = ValidationError;
fn scope(&self) -> &ExecScopeId {
&self.scope
}
fn impact(&self) -> ActionImpact {
self.impact
}
fn metadata(&self) -> &ActionMetadata {
&self.metadata
}
fn execute(&self, _ctx: &ExecutionContext) -> Result<(), ValidationError> {
Ok(())
}
fn content_hash(&self) -> [u8; 32] {
self.content_hash
}
fn make_rollback_not_supported_error() -> ValidationError {
ValidationError::Internal("Rollback not supported for validation".to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatorConfig {
pub default_dim: usize,
pub reflex_threshold: f32,
pub retrieval_threshold: f32,
pub heavy_threshold: f32,
pub include_supporting: bool,
pub create_cross_support_edges: bool,
}
impl Default for ValidatorConfig {
fn default() -> Self {
Self {
default_dim: 384, reflex_threshold: 0.3,
retrieval_threshold: 0.6,
heavy_threshold: 0.9,
include_supporting: true,
create_cross_support_edges: false,
}
}
}
pub struct SheafCoherenceValidator {
gate: CoherenceGate,
config: ValidatorConfig,
policy_ref: Option<PolicyBundleRef>,
}
impl SheafCoherenceValidator {
pub fn new(gate: CoherenceGate) -> Self {
Self {
gate,
config: ValidatorConfig::default(),
policy_ref: None,
}
}
pub fn with_defaults() -> Self {
let policy = PolicyBundleRef {
id: crate::governance::PolicyBundleId::new(),
version: crate::governance::Version::initial(),
content_hash: Hash::zero(),
};
let gate = CoherenceGate::with_defaults(policy.clone().into_execution_ref());
Self {
gate,
config: ValidatorConfig::default(),
policy_ref: Some(policy),
}
}
pub fn with_config(mut self, config: ValidatorConfig) -> Self {
self.config = config;
self
}
pub fn with_policy(mut self, policy: PolicyBundleRef) -> Self {
self.policy_ref = Some(policy);
self
}
pub fn validate(&mut self, context: &ValidationContext) -> Result<ValidationResult, ValidationError> {
context.validate()?;
let graph = self.build_graph(context)?;
let energy = graph.compute_energy();
let energy_snapshot = EnergySnapshot::new(
energy.total_energy,
energy.scope_energy(&context.scope),
ExecScopeId::new(&context.scope),
);
let action = ValidationAction::new(context);
let (decision, _exec_witness) = self.gate.evaluate_with_witness(&action, &energy_snapshot);
let confidence = self.compute_confidence(energy.total_energy);
let witness_decision = if decision.allow {
WitnessDecision::allow(decision.lane.as_u8(), confidence)
} else {
WitnessDecision::deny(
decision.lane.as_u8(),
decision.reason.clone().unwrap_or_else(|| "Energy too high".to_string()),
confidence,
)
};
let witness = ValidationWitness::new(
context,
energy.total_energy,
witness_decision,
self.policy_ref.clone(),
);
let edge_breakdown = self.build_edge_breakdown(&graph, &energy);
let result = if decision.allow {
ValidationResult::allow(energy.total_energy, witness, context.request_id)
} else {
ValidationResult::deny(
energy.total_energy,
decision.reason.unwrap_or_else(|| "Coherence threshold exceeded".to_string()),
witness,
context.request_id,
)
};
Ok(result.with_edge_breakdown(edge_breakdown))
}
fn build_graph(&self, context: &ValidationContext) -> Result<SheafGraph, ValidationError> {
let graph = SheafGraph::new();
let dim = context.embedding_dim();
let context_node = SheafNodeBuilder::new()
.state_from_slice(&context.context_embedding)
.label("context")
.node_type("context")
.namespace(&context.scope)
.build();
let context_id = graph.add_node(context_node);
let response_node = SheafNodeBuilder::new()
.state_from_slice(&context.response_embedding)
.label("response")
.node_type("response")
.namespace(&context.scope)
.build();
let response_id = graph.add_node(response_node);
let ctx_resp_edge = SheafEdgeBuilder::new(context_id, response_id)
.identity_restrictions(dim)
.weight(context.edge_weights.context_response)
.edge_type("context_response")
.namespace(&context.scope)
.build();
graph
.add_edge(ctx_resp_edge)
.map_err(|e| ValidationError::GraphError(e.to_string()))?;
if self.config.include_supporting {
let mut support_ids = Vec::new();
for (i, emb) in context.supporting_embeddings.iter().enumerate() {
let support_node = SheafNodeBuilder::new()
.state_from_slice(emb)
.label(format!("support_{}", i))
.node_type("supporting")
.namespace(&context.scope)
.build();
let support_id = graph.add_node(support_node);
support_ids.push(support_id);
let ctx_sup_edge = SheafEdgeBuilder::new(context_id, support_id)
.identity_restrictions(dim)
.weight(context.edge_weights.context_support)
.edge_type("context_support")
.namespace(&context.scope)
.build();
graph
.add_edge(ctx_sup_edge)
.map_err(|e| ValidationError::GraphError(e.to_string()))?;
let resp_sup_edge = SheafEdgeBuilder::new(response_id, support_id)
.identity_restrictions(dim)
.weight(context.edge_weights.response_support)
.edge_type("response_support")
.namespace(&context.scope)
.build();
graph
.add_edge(resp_sup_edge)
.map_err(|e| ValidationError::GraphError(e.to_string()))?;
}
if self.config.create_cross_support_edges && support_ids.len() > 1 {
for i in 0..support_ids.len() {
for j in (i + 1)..support_ids.len() {
let cross_edge = SheafEdgeBuilder::new(support_ids[i], support_ids[j])
.identity_restrictions(dim)
.weight(context.edge_weights.support_support)
.edge_type("support_support")
.namespace(&context.scope)
.build();
graph
.add_edge(cross_edge)
.map_err(|e| ValidationError::GraphError(e.to_string()))?;
}
}
}
}
Ok(graph)
}
fn build_edge_breakdown(
&self,
graph: &SheafGraph,
energy: &crate::substrate::graph::CoherenceEnergy,
) -> HashMap<String, f32> {
let mut breakdown: HashMap<String, f32> = HashMap::new();
for edge_id in graph.edge_ids() {
if let Some(edge) = graph.get_edge(edge_id) {
let edge_type = edge.edge_type.as_deref().unwrap_or("unknown");
if let Some(&edge_energy) = energy.edge_energies.get(&edge_id) {
*breakdown.entry(edge_type.to_string()).or_insert(0.0) += edge_energy;
}
}
}
breakdown
}
fn compute_confidence(&self, energy: f32) -> f32 {
let normalized = energy / self.config.heavy_threshold;
1.0 / (1.0 + normalized.exp())
}
pub fn config(&self) -> &ValidatorConfig {
&self.config
}
pub fn gate(&self) -> &CoherenceGate {
&self.gate
}
pub fn gate_mut(&mut self) -> &mut CoherenceGate {
&mut self.gate
}
pub fn update_policy(&mut self, policy: PolicyBundleRef) {
self.policy_ref = Some(policy.clone());
self.gate.update_policy_bundle(policy.into_execution_ref());
}
}
impl PolicyBundleRef {
fn into_execution_ref(self) -> crate::execution::PolicyBundleRef {
crate::execution::PolicyBundleRef {
id: self.id.0,
version: format!("{}.{}.{}", self.version.major, self.version.minor, self.version.patch),
content_hash: *self.content_hash.as_bytes(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_embedding(dim: usize, base_value: f32) -> Vec<f32> {
(0..dim).map(|i| base_value + (i as f32) * 0.01).collect()
}
#[test]
fn test_validation_context_creation() {
let ctx = ValidationContext::new()
.with_context_embedding(vec![1.0, 2.0, 3.0])
.with_response_embedding(vec![1.0, 2.0, 3.0])
.with_scope("test")
.with_metadata("test_key", "test_value");
assert_eq!(ctx.embedding_dim(), 3);
assert!(ctx.validate().is_ok());
}
#[test]
fn test_validation_context_dimension_mismatch() {
let ctx = ValidationContext::new()
.with_context_embedding(vec![1.0, 2.0, 3.0])
.with_response_embedding(vec![1.0, 2.0]);
let result = ctx.validate();
assert!(matches!(result, Err(ValidationError::DimensionMismatch { .. })));
}
#[test]
fn test_edge_weights() {
let strict = EdgeWeights::strict();
assert!(strict.context_response > 1.0);
let permissive = EdgeWeights::permissive();
assert!(permissive.context_response <= 1.0);
}
#[test]
fn test_validation_witness_integrity() {
let ctx = ValidationContext::new()
.with_context_embedding(vec![1.0, 2.0, 3.0])
.with_response_embedding(vec![1.0, 2.0, 3.0]);
let witness = ValidationWitness::new(
&ctx,
0.5,
WitnessDecision::allow(0, 0.9),
None,
);
assert!(witness.verify_integrity());
}
#[test]
fn test_validator_coherent_response() {
let mut validator = SheafCoherenceValidator::with_defaults();
let ctx = ValidationContext::new()
.with_context_embedding(create_test_embedding(64, 1.0))
.with_response_embedding(create_test_embedding(64, 1.0));
let result = validator.validate(&ctx).unwrap();
assert!(result.allowed);
assert!(result.energy < 0.01); }
#[test]
fn test_validator_incoherent_response() {
let mut validator = SheafCoherenceValidator::with_defaults()
.with_config(ValidatorConfig {
reflex_threshold: 0.01, ..Default::default()
});
let ctx = ValidationContext::new()
.with_context_embedding(create_test_embedding(64, 1.0))
.with_response_embedding(create_test_embedding(64, 100.0));
let result = validator.validate(&ctx).unwrap();
assert!(result.energy > 0.0);
}
#[test]
fn test_validator_with_supporting() {
let mut validator = SheafCoherenceValidator::with_defaults();
let ctx = ValidationContext::new()
.with_context_embedding(create_test_embedding(64, 1.0))
.with_response_embedding(create_test_embedding(64, 1.0))
.with_supporting_embedding(create_test_embedding(64, 1.0))
.with_supporting_embedding(create_test_embedding(64, 1.0));
let result = validator.validate(&ctx).unwrap();
assert!(result.allowed);
assert!(!result.edge_breakdown.is_empty());
}
#[test]
fn test_validation_result_serialization() {
let ctx = ValidationContext::new()
.with_context_embedding(vec![1.0, 2.0, 3.0])
.with_response_embedding(vec![1.0, 2.0, 3.0]);
let witness = ValidationWitness::new(
&ctx,
0.1,
WitnessDecision::allow(0, 0.95),
None,
);
let result = ValidationResult::allow(0.1, witness, ctx.request_id);
let json = serde_json::to_string(&result).unwrap();
let deserialized: ValidationResult = serde_json::from_str(&json).unwrap();
assert_eq!(result.energy, deserialized.energy);
assert_eq!(result.allowed, deserialized.allowed);
}
}