Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,872 @@
//! Lineage Record Entity
//!
//! Implements provenance tracking for all authoritative writes.
//!
//! # Core Invariant
//!
//! **No write without lineage**: Every authoritative write MUST have a lineage record
//! that tracks:
//!
//! - What entity was modified
//! - What operation was performed
//! - What witness authorized the write
//! - Who performed the write
//! - What prior lineage records this depends on
//!
//! # Causal Dependencies
//!
//! Lineage records form a directed acyclic graph (DAG) of dependencies:
//!
//! ```text
//! L1 ─────┐
//! ├──► L4 ──► L5
//! L2 ─────┤
//! └──► L6
//! L3 ──────────────► L7
//! ```
//!
//! This enables:
//! - Understanding the causal history of any entity
//! - Detecting concurrent writes
//! - Supporting deterministic replay
use super::{Hash, Timestamp, WitnessId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use uuid::Uuid;
/// Unique identifier for a lineage record
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct LineageId(pub Uuid);
impl LineageId {
/// Generate a new random ID
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Create from a UUID
#[must_use]
pub const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
/// Get as bytes
#[must_use]
pub fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
/// Create a nil/sentinel ID
#[must_use]
pub const fn nil() -> Self {
Self(Uuid::nil())
}
/// Check if this is the nil ID
#[must_use]
pub fn is_nil(&self) -> bool {
self.0.is_nil()
}
}
impl Default for LineageId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for LineageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Reference to an entity in the system
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EntityRef {
/// Entity type (e.g., "node", "edge", "policy")
pub entity_type: String,
/// Entity identifier
pub entity_id: String,
/// Optional namespace/scope
pub namespace: Option<String>,
/// Version of the entity (if applicable)
pub version: Option<u64>,
}
impl EntityRef {
/// Create a new entity reference
#[must_use]
pub fn new(entity_type: impl Into<String>, entity_id: impl Into<String>) -> Self {
Self {
entity_type: entity_type.into(),
entity_id: entity_id.into(),
namespace: None,
version: None,
}
}
/// Set the namespace
#[must_use]
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
self.namespace = Some(namespace.into());
self
}
/// Set the version
#[must_use]
pub const fn with_version(mut self, version: u64) -> Self {
self.version = Some(version);
self
}
/// Create a node reference
#[must_use]
pub fn node(id: impl Into<String>) -> Self {
Self::new("node", id)
}
/// Create an edge reference
#[must_use]
pub fn edge(id: impl Into<String>) -> Self {
Self::new("edge", id)
}
/// Create a policy reference
#[must_use]
pub fn policy(id: impl Into<String>) -> Self {
Self::new("policy", id)
}
/// Get a canonical string representation
#[must_use]
pub fn canonical(&self) -> String {
let mut s = format!("{}:{}", self.entity_type, self.entity_id);
if let Some(ref ns) = self.namespace {
s = format!("{ns}/{s}");
}
if let Some(v) = self.version {
s = format!("{s}@{v}");
}
s
}
/// Compute content hash
#[must_use]
pub fn content_hash(&self) -> Hash {
let mut hasher = blake3::Hasher::new();
hasher.update(self.entity_type.as_bytes());
hasher.update(self.entity_id.as_bytes());
if let Some(ref ns) = self.namespace {
hasher.update(ns.as_bytes());
}
if let Some(v) = self.version {
hasher.update(&v.to_le_bytes());
}
Hash::from_blake3(hasher.finalize())
}
}
impl std::fmt::Display for EntityRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.canonical())
}
}
/// Type of operation performed on an entity
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Operation {
/// Create a new entity
Create,
/// Update an existing entity
Update,
/// Delete an entity
Delete,
/// Archive an entity (soft delete)
Archive,
/// Restore an archived entity
Restore,
/// Merge entities
Merge,
/// Split an entity
Split,
/// Transfer ownership
Transfer,
}
impl Operation {
/// Check if this operation creates a new entity
#[must_use]
pub const fn is_create(&self) -> bool {
matches!(self, Self::Create | Self::Split)
}
/// Check if this operation removes an entity
#[must_use]
pub const fn is_destructive(&self) -> bool {
matches!(self, Self::Delete | Self::Archive | Self::Merge)
}
/// Check if this operation modifies an entity
#[must_use]
pub const fn is_mutation(&self) -> bool {
matches!(
self,
Self::Update | Self::Transfer | Self::Restore | Self::Merge | Self::Split
)
}
}
impl std::fmt::Display for Operation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Create => write!(f, "CREATE"),
Self::Update => write!(f, "UPDATE"),
Self::Delete => write!(f, "DELETE"),
Self::Archive => write!(f, "ARCHIVE"),
Self::Restore => write!(f, "RESTORE"),
Self::Merge => write!(f, "MERGE"),
Self::Split => write!(f, "SPLIT"),
Self::Transfer => write!(f, "TRANSFER"),
}
}
}
/// Lineage-related errors
#[derive(Debug, Error)]
pub enum LineageError {
/// Missing authorizing witness
#[error("Missing authorizing witness for lineage {0}")]
MissingWitness(LineageId),
/// Dependency not found
#[error("Dependency not found: {0}")]
DependencyNotFound(LineageId),
/// Circular dependency detected
#[error("Circular dependency detected involving {0}")]
CircularDependency(LineageId),
/// Invalid operation for entity state
#[error("Invalid operation {0} for entity {1}")]
InvalidOperation(Operation, EntityRef),
/// Lineage not found
#[error("Lineage not found: {0}")]
NotFound(LineageId),
/// Lineage already exists
#[error("Lineage already exists: {0}")]
AlreadyExists(LineageId),
/// Content hash mismatch
#[error("Content hash mismatch for lineage {0}")]
HashMismatch(LineageId),
}
/// Provenance tracking for an authoritative write
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LineageRecord {
/// Unique lineage identifier
pub id: LineageId,
/// Entity that was modified
pub entity_ref: EntityRef,
/// Operation performed
pub operation: Operation,
/// Causal dependencies (prior lineage records this depends on)
pub dependencies: Vec<LineageId>,
/// Witness that authorized this write
pub authorizing_witness: WitnessId,
/// Actor who performed the write
pub actor: String,
/// Creation timestamp
pub timestamp: Timestamp,
/// Content hash for integrity
pub content_hash: Hash,
/// Optional description of the change
pub description: Option<String>,
/// Optional previous state hash (for updates)
pub previous_state_hash: Option<Hash>,
/// Optional new state hash
pub new_state_hash: Option<Hash>,
/// Additional metadata
pub metadata: HashMap<String, String>,
}
impl LineageRecord {
/// Create a new lineage record
#[must_use]
pub fn new(
entity_ref: EntityRef,
operation: Operation,
dependencies: Vec<LineageId>,
authorizing_witness: WitnessId,
actor: impl Into<String>,
) -> Self {
let id = LineageId::new();
let timestamp = Timestamp::now();
let mut record = Self {
id,
entity_ref,
operation,
dependencies,
authorizing_witness,
actor: actor.into(),
timestamp,
content_hash: Hash::zero(), // Placeholder
description: None,
previous_state_hash: None,
new_state_hash: None,
metadata: HashMap::new(),
};
record.content_hash = record.compute_content_hash();
record
}
/// Create a lineage record for entity creation
#[must_use]
pub fn create(
entity_ref: EntityRef,
authorizing_witness: WitnessId,
actor: impl Into<String>,
) -> Self {
Self::new(
entity_ref,
Operation::Create,
Vec::new(),
authorizing_witness,
actor,
)
}
/// Create a lineage record for entity update
#[must_use]
pub fn update(
entity_ref: EntityRef,
dependencies: Vec<LineageId>,
authorizing_witness: WitnessId,
actor: impl Into<String>,
) -> Self {
Self::new(
entity_ref,
Operation::Update,
dependencies,
authorizing_witness,
actor,
)
}
/// Create a lineage record for entity deletion
#[must_use]
pub fn delete(
entity_ref: EntityRef,
dependencies: Vec<LineageId>,
authorizing_witness: WitnessId,
actor: impl Into<String>,
) -> Self {
Self::new(
entity_ref,
Operation::Delete,
dependencies,
authorizing_witness,
actor,
)
}
/// Set description
#[must_use]
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self.content_hash = self.compute_content_hash();
self
}
/// Set previous state hash
#[must_use]
pub fn with_previous_state(mut self, hash: Hash) -> Self {
self.previous_state_hash = Some(hash);
self.content_hash = self.compute_content_hash();
self
}
/// Set new state hash
#[must_use]
pub fn with_new_state(mut self, hash: Hash) -> Self {
self.new_state_hash = Some(hash);
self.content_hash = self.compute_content_hash();
self
}
/// Add metadata
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self.content_hash = self.compute_content_hash();
self
}
/// Compute the content hash using Blake3
#[must_use]
pub fn compute_content_hash(&self) -> Hash {
let mut hasher = blake3::Hasher::new();
// Core identifying fields
hasher.update(self.id.as_bytes());
hasher.update(self.entity_ref.content_hash().as_bytes());
hasher.update(&[self.operation as u8]);
// Dependencies (sorted for determinism)
let mut deps: Vec<_> = self.dependencies.iter().collect();
deps.sort_by_key(|d| d.0);
for dep in deps {
hasher.update(dep.as_bytes());
}
// Authorization
hasher.update(self.authorizing_witness.as_bytes());
hasher.update(self.actor.as_bytes());
// Timestamp
hasher.update(&self.timestamp.secs.to_le_bytes());
hasher.update(&self.timestamp.nanos.to_le_bytes());
// Optional fields
if let Some(ref desc) = self.description {
hasher.update(desc.as_bytes());
}
if let Some(ref prev) = self.previous_state_hash {
hasher.update(prev.as_bytes());
}
if let Some(ref new) = self.new_state_hash {
hasher.update(new.as_bytes());
}
// Metadata (sorted for determinism)
let mut meta_keys: Vec<_> = self.metadata.keys().collect();
meta_keys.sort();
for key in meta_keys {
hasher.update(key.as_bytes());
if let Some(value) = self.metadata.get(key) {
hasher.update(value.as_bytes());
}
}
Hash::from_blake3(hasher.finalize())
}
/// Verify the content hash is correct
#[must_use]
pub fn verify_content_hash(&self) -> bool {
self.content_hash == self.compute_content_hash()
}
/// Check if this lineage has no dependencies (root lineage)
#[must_use]
pub fn is_root(&self) -> bool {
self.dependencies.is_empty()
}
/// Check if this lineage depends on a specific lineage
#[must_use]
pub fn depends_on(&self, other: LineageId) -> bool {
self.dependencies.contains(&other)
}
}
impl PartialEq for LineageRecord {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for LineageRecord {}
impl std::hash::Hash for LineageRecord {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// Builder for lineage records with validation
pub struct LineageBuilder {
entity_ref: Option<EntityRef>,
operation: Option<Operation>,
dependencies: Vec<LineageId>,
authorizing_witness: Option<WitnessId>,
actor: Option<String>,
description: Option<String>,
previous_state_hash: Option<Hash>,
new_state_hash: Option<Hash>,
metadata: HashMap<String, String>,
}
impl LineageBuilder {
/// Create a new builder
#[must_use]
pub fn new() -> Self {
Self {
entity_ref: None,
operation: None,
dependencies: Vec::new(),
authorizing_witness: None,
actor: None,
description: None,
previous_state_hash: None,
new_state_hash: None,
metadata: HashMap::new(),
}
}
/// Set the entity reference
#[must_use]
pub fn entity(mut self, entity_ref: EntityRef) -> Self {
self.entity_ref = Some(entity_ref);
self
}
/// Set the operation
#[must_use]
pub fn operation(mut self, op: Operation) -> Self {
self.operation = Some(op);
self
}
/// Add a dependency
#[must_use]
pub fn depends_on(mut self, dep: LineageId) -> Self {
self.dependencies.push(dep);
self
}
/// Set all dependencies
#[must_use]
pub fn dependencies(mut self, deps: Vec<LineageId>) -> Self {
self.dependencies = deps;
self
}
/// Set the authorizing witness
#[must_use]
pub fn authorized_by(mut self, witness: WitnessId) -> Self {
self.authorizing_witness = Some(witness);
self
}
/// Set the actor
#[must_use]
pub fn actor(mut self, actor: impl Into<String>) -> Self {
self.actor = Some(actor.into());
self
}
/// Set description
#[must_use]
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
/// Set previous state hash
#[must_use]
pub fn previous_state(mut self, hash: Hash) -> Self {
self.previous_state_hash = Some(hash);
self
}
/// Set new state hash
#[must_use]
pub fn new_state(mut self, hash: Hash) -> Self {
self.new_state_hash = Some(hash);
self
}
/// Add metadata
#[must_use]
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
/// Build the lineage record
///
/// # Errors
///
/// Returns error if required fields are missing
pub fn build(self) -> Result<LineageRecord, LineageError> {
let entity_ref = self.entity_ref.ok_or_else(|| {
LineageError::InvalidOperation(
self.operation.unwrap_or(Operation::Create),
EntityRef::new("unknown", "unknown"),
)
})?;
let operation = self.operation.unwrap_or(Operation::Create);
let authorizing_witness = self
.authorizing_witness
.ok_or_else(|| LineageError::MissingWitness(LineageId::nil()))?;
let actor = self.actor.unwrap_or_else(|| "unknown".to_string());
let mut record = LineageRecord::new(
entity_ref,
operation,
self.dependencies,
authorizing_witness,
actor,
);
if let Some(desc) = self.description {
record = record.with_description(desc);
}
if let Some(prev) = self.previous_state_hash {
record = record.with_previous_state(prev);
}
if let Some(new) = self.new_state_hash {
record = record.with_new_state(new);
}
for (key, value) in self.metadata {
record = record.with_metadata(key, value);
}
Ok(record)
}
}
impl Default for LineageBuilder {
fn default() -> Self {
Self::new()
}
}
/// Tracks lineage for an entity across multiple operations
pub struct EntityLineageTracker {
/// Entity being tracked
pub entity_ref: EntityRef,
/// All lineage records for this entity (ordered by timestamp)
pub lineage: Vec<LineageRecord>,
/// Current state hash
pub current_state_hash: Option<Hash>,
}
impl EntityLineageTracker {
/// Create a new tracker
#[must_use]
pub fn new(entity_ref: EntityRef) -> Self {
Self {
entity_ref,
lineage: Vec::new(),
current_state_hash: None,
}
}
/// Add a lineage record
///
/// # Errors
///
/// Returns error if the record is for a different entity
pub fn add(&mut self, record: LineageRecord) -> Result<(), LineageError> {
if record.entity_ref != self.entity_ref {
return Err(LineageError::InvalidOperation(
record.operation,
self.entity_ref.clone(),
));
}
// Update current state hash
if let Some(ref new_hash) = record.new_state_hash {
self.current_state_hash = Some(*new_hash);
}
// Insert in timestamp order
let pos = self
.lineage
.iter()
.position(|r| r.timestamp > record.timestamp)
.unwrap_or(self.lineage.len());
self.lineage.insert(pos, record);
Ok(())
}
/// Get the most recent lineage record
#[must_use]
pub fn latest(&self) -> Option<&LineageRecord> {
self.lineage.last()
}
/// Get all dependencies for this entity
#[must_use]
pub fn all_dependencies(&self) -> Vec<LineageId> {
self.lineage
.iter()
.flat_map(|r| r.dependencies.iter().copied())
.collect()
}
/// Check if the entity has been deleted
#[must_use]
pub fn is_deleted(&self) -> bool {
self.lineage
.last()
.map_or(false, |r| r.operation == Operation::Delete)
}
/// Get lineage records by operation type
#[must_use]
pub fn by_operation(&self, op: Operation) -> Vec<&LineageRecord> {
self.lineage.iter().filter(|r| r.operation == op).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_witness_id() -> WitnessId {
WitnessId::new()
}
#[test]
fn test_entity_ref() {
let entity = EntityRef::node("node-123")
.with_namespace("test")
.with_version(1);
assert_eq!(entity.entity_type, "node");
assert_eq!(entity.entity_id, "node-123");
assert_eq!(entity.namespace, Some("test".to_string()));
assert_eq!(entity.version, Some(1));
assert_eq!(entity.canonical(), "test/node:node-123@1");
}
#[test]
fn test_lineage_creation() {
let entity = EntityRef::node("node-1");
let witness = test_witness_id();
let lineage = LineageRecord::create(entity.clone(), witness, "alice");
assert_eq!(lineage.operation, Operation::Create);
assert!(lineage.is_root());
assert!(lineage.verify_content_hash());
}
#[test]
fn test_lineage_with_dependencies() {
let entity = EntityRef::node("node-1");
let witness = test_witness_id();
let dep1 = LineageId::new();
let dep2 = LineageId::new();
let lineage = LineageRecord::update(entity, vec![dep1, dep2], witness, "bob");
assert!(!lineage.is_root());
assert!(lineage.depends_on(dep1));
assert!(lineage.depends_on(dep2));
}
#[test]
fn test_lineage_builder() -> Result<(), LineageError> {
let lineage = LineageBuilder::new()
.entity(EntityRef::edge("edge-1"))
.operation(Operation::Update)
.authorized_by(test_witness_id())
.actor("charlie")
.description("Updated edge weight")
.previous_state(Hash::from_bytes([1u8; 32]))
.new_state(Hash::from_bytes([2u8; 32]))
.metadata("reason", "optimization")
.build()?;
assert_eq!(lineage.operation, Operation::Update);
assert!(lineage.description.is_some());
assert!(lineage.previous_state_hash.is_some());
assert!(lineage.new_state_hash.is_some());
assert_eq!(
lineage.metadata.get("reason"),
Some(&"optimization".to_string())
);
Ok(())
}
#[test]
fn test_entity_lineage_tracker() -> Result<(), LineageError> {
let entity = EntityRef::node("node-1");
let witness = test_witness_id();
let mut tracker = EntityLineageTracker::new(entity.clone());
// Create
let create = LineageRecord::create(entity.clone(), witness, "alice")
.with_new_state(Hash::from_bytes([1u8; 32]));
tracker.add(create)?;
// Update
let update = LineageRecord::update(
entity.clone(),
vec![tracker.latest().unwrap().id],
witness,
"bob",
)
.with_previous_state(Hash::from_bytes([1u8; 32]))
.with_new_state(Hash::from_bytes([2u8; 32]));
tracker.add(update)?;
assert_eq!(tracker.lineage.len(), 2);
assert_eq!(
tracker.current_state_hash,
Some(Hash::from_bytes([2u8; 32]))
);
assert!(!tracker.is_deleted());
Ok(())
}
#[test]
fn test_content_hash_determinism() {
let entity = EntityRef::node("node-1");
let witness = test_witness_id();
let lineage = LineageRecord::create(entity, witness, "alice").with_description("test");
let hash1 = lineage.compute_content_hash();
let hash2 = lineage.compute_content_hash();
assert_eq!(hash1, hash2);
}
#[test]
fn test_tamper_detection() {
let entity = EntityRef::node("node-1");
let witness = test_witness_id();
let mut lineage = LineageRecord::create(entity, witness, "alice");
// Tamper with the record
lineage.actor = "mallory".to_string();
// Hash should no longer match
assert!(!lineage.verify_content_hash());
}
#[test]
fn test_operation_classification() {
assert!(Operation::Create.is_create());
assert!(Operation::Split.is_create());
assert!(!Operation::Update.is_create());
assert!(Operation::Delete.is_destructive());
assert!(Operation::Archive.is_destructive());
assert!(!Operation::Create.is_destructive());
assert!(Operation::Update.is_mutation());
assert!(!Operation::Create.is_mutation());
}
}

View File

@@ -0,0 +1,438 @@
//! Governance Layer
//!
//! First-class, immutable, addressable governance objects for the Coherence Engine.
//!
//! This module implements ADR-CE-005: "Governance objects are first-class, immutable, addressable"
//!
//! # Core Invariants
//!
//! 1. **No action without witness**: Every gate decision must produce a `WitnessRecord`
//! 2. **No write without lineage**: Every authoritative write must have a `LineageRecord`
//! 3. **Policy immutability**: Once activated, a `PolicyBundle` cannot be modified
//! 4. **Multi-party approval**: Critical policies require multiple `ApprovalSignature`s
//! 5. **Witness chain integrity**: Each witness references its predecessor via Blake3 hash
mod lineage;
mod policy;
mod repository;
mod witness;
pub use policy::{
ApprovalSignature, ApproverId, EscalationCondition, EscalationRule, PolicyBundle,
PolicyBundleBuilder, PolicyBundleId, PolicyBundleRef, PolicyBundleStatus, PolicyError,
ThresholdConfig,
};
pub use witness::{
ComputeLane as WitnessComputeLane, EnergySnapshot, GateDecision, WitnessChainError,
WitnessError, WitnessId, WitnessRecord,
};
pub use lineage::{EntityRef, LineageError, LineageId, LineageRecord, Operation};
pub use repository::{LineageRepository, PolicyRepository, WitnessRepository};
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
/// Blake3 content hash (32 bytes)
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Hash(pub [u8; 32]);
impl Hash {
/// Create a new hash from bytes
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
/// Create a hash from a Blake3 hasher output
#[must_use]
pub fn from_blake3(hash: blake3::Hash) -> Self {
Self(*hash.as_bytes())
}
/// Get the hash as bytes
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
/// Create a zero hash (used as sentinel)
#[must_use]
pub const fn zero() -> Self {
Self([0u8; 32])
}
/// Check if this is the zero hash
#[must_use]
pub fn is_zero(&self) -> bool {
self.0 == [0u8; 32]
}
/// Convert to hex string
#[must_use]
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
/// Parse from hex string
///
/// # Errors
///
/// Returns an error if the hex string is invalid or wrong length
pub fn from_hex(s: &str) -> Result<Self, hex::FromHexError> {
let bytes = hex::decode(s)?;
if bytes.len() != 32 {
return Err(hex::FromHexError::InvalidStringLength);
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(Self(arr))
}
}
impl fmt::Debug for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Hash({})", &self.to_hex()[..16])
}
}
impl fmt::Display for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl Default for Hash {
fn default() -> Self {
Self::zero()
}
}
impl From<blake3::Hash> for Hash {
fn from(hash: blake3::Hash) -> Self {
Self::from_blake3(hash)
}
}
impl AsRef<[u8]> for Hash {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
/// Timestamp with nanosecond precision
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Timestamp {
/// Seconds since Unix epoch
pub secs: i64,
/// Nanoseconds within the second
pub nanos: u32,
}
impl Timestamp {
/// Create a new timestamp
#[must_use]
pub const fn new(secs: i64, nanos: u32) -> Self {
Self { secs, nanos }
}
/// Get the current timestamp
#[must_use]
pub fn now() -> Self {
let dt = chrono::Utc::now();
Self {
secs: dt.timestamp(),
nanos: dt.timestamp_subsec_nanos(),
}
}
/// Create a timestamp from Unix epoch seconds
#[must_use]
pub const fn from_secs(secs: i64) -> Self {
Self { secs, nanos: 0 }
}
/// Convert to Unix epoch milliseconds
#[must_use]
pub const fn as_millis(&self) -> i64 {
self.secs * 1000 + (self.nanos / 1_000_000) as i64
}
/// Create from Unix epoch milliseconds
#[must_use]
pub const fn from_millis(millis: i64) -> Self {
Self {
secs: millis / 1000,
nanos: ((millis % 1000) * 1_000_000) as u32,
}
}
/// Convert to chrono DateTime
#[must_use]
pub fn to_datetime(&self) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::from_timestamp(self.secs, self.nanos).unwrap_or_else(chrono::Utc::now)
}
}
impl Default for Timestamp {
fn default() -> Self {
Self::now()
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
self.to_datetime().format("%Y-%m-%d %H:%M:%S%.3f UTC")
)
}
}
impl From<chrono::DateTime<chrono::Utc>> for Timestamp {
fn from(dt: chrono::DateTime<chrono::Utc>) -> Self {
Self {
secs: dt.timestamp(),
nanos: dt.timestamp_subsec_nanos(),
}
}
}
/// Semantic version for policy bundles
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Version {
/// Major version (breaking changes)
pub major: u32,
/// Minor version (new features, backward compatible)
pub minor: u32,
/// Patch version (bug fixes)
pub patch: u32,
}
impl Version {
/// Create a new version
#[must_use]
pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
/// Initial version (1.0.0)
#[must_use]
pub const fn initial() -> Self {
Self::new(1, 0, 0)
}
/// Increment patch version
#[must_use]
pub const fn bump_patch(self) -> Self {
Self {
major: self.major,
minor: self.minor,
patch: self.patch + 1,
}
}
/// Increment minor version (resets patch)
#[must_use]
pub const fn bump_minor(self) -> Self {
Self {
major: self.major,
minor: self.minor + 1,
patch: 0,
}
}
/// Increment major version (resets minor and patch)
#[must_use]
pub const fn bump_major(self) -> Self {
Self {
major: self.major + 1,
minor: 0,
patch: 0,
}
}
}
impl Default for Version {
fn default() -> Self {
Self::initial()
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl std::str::FromStr for Version {
type Err = GovernanceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return Err(GovernanceError::InvalidVersion(s.to_string()));
}
let major = parts[0]
.parse()
.map_err(|_| GovernanceError::InvalidVersion(s.to_string()))?;
let minor = parts[1]
.parse()
.map_err(|_| GovernanceError::InvalidVersion(s.to_string()))?;
let patch = parts[2]
.parse()
.map_err(|_| GovernanceError::InvalidVersion(s.to_string()))?;
Ok(Self {
major,
minor,
patch,
})
}
}
/// Top-level governance error
#[derive(Debug, Error)]
pub enum GovernanceError {
/// Policy-related error
#[error("Policy error: {0}")]
Policy(#[from] PolicyError),
/// Witness-related error
#[error("Witness error: {0}")]
Witness(#[from] WitnessError),
/// Lineage-related error
#[error("Lineage error: {0}")]
Lineage(#[from] LineageError),
/// Invalid version format
#[error("Invalid version format: {0}")]
InvalidVersion(String),
/// Serialization error
#[error("Serialization error: {0}")]
Serialization(String),
/// Repository error
#[error("Repository error: {0}")]
Repository(String),
/// Invariant violation
#[error("Invariant violation: {0}")]
InvariantViolation(String),
}
// Hex encoding utilities (inline to avoid external dependency)
mod hex {
pub use std::fmt::Write;
#[derive(Debug)]
pub enum FromHexError {
InvalidStringLength,
InvalidHexCharacter(char),
}
impl std::fmt::Display for FromHexError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidStringLength => write!(f, "invalid hex string length"),
Self::InvalidHexCharacter(c) => write!(f, "invalid hex character: {c}"),
}
}
}
impl std::error::Error for FromHexError {}
pub fn encode(bytes: impl AsRef<[u8]>) -> String {
let bytes = bytes.as_ref();
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
write!(s, "{b:02x}").unwrap();
}
s
}
pub fn decode(s: &str) -> Result<Vec<u8>, FromHexError> {
if s.len() % 2 != 0 {
return Err(FromHexError::InvalidStringLength);
}
let mut bytes = Vec::with_capacity(s.len() / 2);
let mut chars = s.chars();
while let (Some(h), Some(l)) = (chars.next(), chars.next()) {
let high = h.to_digit(16).ok_or(FromHexError::InvalidHexCharacter(h))? as u8;
let low = l.to_digit(16).ok_or(FromHexError::InvalidHexCharacter(l))? as u8;
bytes.push((high << 4) | low);
}
Ok(bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_creation_and_display() {
let bytes = [1u8; 32];
let hash = Hash::from_bytes(bytes);
assert_eq!(hash.as_bytes(), &bytes);
assert!(!hash.is_zero());
let hex = hash.to_hex();
let parsed = Hash::from_hex(&hex).unwrap();
assert_eq!(hash, parsed);
}
#[test]
fn test_hash_zero() {
let zero = Hash::zero();
assert!(zero.is_zero());
assert_eq!(zero.as_bytes(), &[0u8; 32]);
}
#[test]
fn test_timestamp() {
let ts = Timestamp::now();
assert!(ts.secs > 0);
let from_secs = Timestamp::from_secs(1700000000);
assert_eq!(from_secs.secs, 1700000000);
assert_eq!(from_secs.nanos, 0);
let from_millis = Timestamp::from_millis(1700000000123);
assert_eq!(from_millis.secs, 1700000000);
assert_eq!(from_millis.nanos, 123_000_000);
}
#[test]
fn test_version() {
let v = Version::new(1, 2, 3);
assert_eq!(v.to_string(), "1.2.3");
let parsed: Version = "2.3.4".parse().unwrap();
assert_eq!(parsed, Version::new(2, 3, 4));
let bumped = Version::new(1, 2, 3).bump_patch();
assert_eq!(bumped, Version::new(1, 2, 4));
let minor_bump = Version::new(1, 2, 3).bump_minor();
assert_eq!(minor_bump, Version::new(1, 3, 0));
let major_bump = Version::new(1, 2, 3).bump_major();
assert_eq!(major_bump, Version::new(2, 0, 0));
}
}

View File

@@ -0,0 +1,969 @@
//! Policy Bundle Aggregate
//!
//! Implements versioned, signed policy bundles with multi-signature threshold configurations.
//!
//! # Lifecycle
//!
//! 1. **Draft**: Initial creation, can be modified
//! 2. **Pending**: Awaiting required approvals
//! 3. **Active**: Fully approved and immutable
//! 4. **Superseded**: Replaced by a newer version
//! 5. **Revoked**: Explicitly invalidated
//!
//! # Immutability Invariant
//!
//! Once a policy bundle reaches `Active` status, it becomes immutable.
//! Any changes require creating a new version.
use super::{Hash, Timestamp, Version};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
use thiserror::Error;
use uuid::Uuid;
/// Unique identifier for a policy bundle
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PolicyBundleId(pub Uuid);
impl PolicyBundleId {
/// Generate a new random ID
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Create from a UUID
#[must_use]
pub const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
/// Get as bytes
#[must_use]
pub fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
}
impl Default for PolicyBundleId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for PolicyBundleId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Lightweight reference to a policy bundle for embedding in other records
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PolicyBundleRef {
/// Bundle ID
pub id: PolicyBundleId,
/// Version at time of reference
pub version: Version,
/// Content hash for integrity verification
pub content_hash: Hash,
}
impl PolicyBundleRef {
/// Create a reference from a policy bundle
#[must_use]
pub fn from_bundle(bundle: &PolicyBundle) -> Self {
Self {
id: bundle.id,
version: bundle.version.clone(),
content_hash: bundle.content_hash(),
}
}
/// Get as bytes for hashing
#[must_use]
pub fn as_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(48 + 12);
bytes.extend_from_slice(self.id.as_bytes());
bytes.extend_from_slice(&self.version.major.to_le_bytes());
bytes.extend_from_slice(&self.version.minor.to_le_bytes());
bytes.extend_from_slice(&self.version.patch.to_le_bytes());
bytes.extend_from_slice(self.content_hash.as_bytes());
bytes
}
}
/// Status of a policy bundle in its lifecycle
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PolicyBundleStatus {
/// Initial creation, can be modified
Draft,
/// Awaiting required approvals
Pending,
/// Fully approved and immutable
Active,
/// Replaced by a newer version
Superseded,
/// Explicitly invalidated
Revoked,
}
impl PolicyBundleStatus {
/// Check if the policy is in an editable state
#[must_use]
pub const fn is_editable(&self) -> bool {
matches!(self, Self::Draft)
}
/// Check if the policy is currently enforceable
#[must_use]
pub const fn is_enforceable(&self) -> bool {
matches!(self, Self::Active)
}
/// Check if the policy is in a terminal state
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(self, Self::Superseded | Self::Revoked)
}
}
/// Unique identifier for an approver (could be a user, service, or key)
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ApproverId(pub String);
impl ApproverId {
/// Create a new approver ID
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
/// Get as string slice
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ApproverId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for ApproverId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<String> for ApproverId {
fn from(s: String) -> Self {
Self(s)
}
}
/// Digital signature for policy approval
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovalSignature {
/// The approver who signed
pub approver_id: ApproverId,
/// Timestamp of approval
pub timestamp: Timestamp,
/// Signature bytes (format depends on signing algorithm)
pub signature: Vec<u8>,
/// Algorithm used (e.g., "ed25519", "secp256k1")
pub algorithm: String,
/// Optional comment from approver
pub comment: Option<String>,
}
impl ApprovalSignature {
/// Create a new approval signature
#[must_use]
pub fn new(approver_id: ApproverId, signature: Vec<u8>, algorithm: impl Into<String>) -> Self {
Self {
approver_id,
timestamp: Timestamp::now(),
signature,
algorithm: algorithm.into(),
comment: None,
}
}
/// Add a comment to the approval
#[must_use]
pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
self.comment = Some(comment.into());
self
}
/// Create a placeholder signature for testing (NOT for production)
#[must_use]
pub fn placeholder(approver_id: ApproverId) -> Self {
Self {
approver_id,
timestamp: Timestamp::now(),
signature: vec![0u8; 64],
algorithm: "placeholder".to_string(),
comment: Some("Test signature".to_string()),
}
}
}
/// Threshold configuration for a scope
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ThresholdConfig {
/// Energy threshold for Lane 0 (Reflex) - allow without additional checks
pub reflex: f32,
/// Energy threshold for Lane 1 (Retrieval) - require evidence fetching
pub retrieval: f32,
/// Energy threshold for Lane 2 (Heavy) - require deep reasoning
pub heavy: f32,
/// Duration for which incoherence must persist before escalation
pub persistence_window: Duration,
/// Optional custom thresholds for specific metrics
pub custom_thresholds: HashMap<String, f32>,
}
impl ThresholdConfig {
/// Create a new threshold config with defaults
#[must_use]
pub fn new(reflex: f32, retrieval: f32, heavy: f32) -> Self {
Self {
reflex,
retrieval,
heavy,
persistence_window: Duration::from_secs(30),
custom_thresholds: HashMap::new(),
}
}
/// Create a strict threshold config (lower thresholds = more escalations)
#[must_use]
pub fn strict() -> Self {
Self {
reflex: 0.1,
retrieval: 0.3,
heavy: 0.6,
persistence_window: Duration::from_secs(10),
custom_thresholds: HashMap::new(),
}
}
/// Create a permissive threshold config (higher thresholds = fewer escalations)
#[must_use]
pub fn permissive() -> Self {
Self {
reflex: 0.5,
retrieval: 0.8,
heavy: 0.95,
persistence_window: Duration::from_secs(60),
custom_thresholds: HashMap::new(),
}
}
/// Set a custom threshold
#[must_use]
pub fn with_custom(mut self, name: impl Into<String>, value: f32) -> Self {
self.custom_thresholds.insert(name.into(), value);
self
}
/// Set persistence window
#[must_use]
pub const fn with_persistence_window(mut self, window: Duration) -> Self {
self.persistence_window = window;
self
}
/// Validate threshold ordering (reflex < retrieval < heavy)
#[must_use]
pub fn is_valid(&self) -> bool {
self.reflex >= 0.0
&& self.reflex <= self.retrieval
&& self.retrieval <= self.heavy
&& self.heavy <= 1.0
}
}
impl Default for ThresholdConfig {
fn default() -> Self {
Self {
reflex: 0.3,
retrieval: 0.6,
heavy: 0.9,
persistence_window: Duration::from_secs(30),
custom_thresholds: HashMap::new(),
}
}
}
/// Rule for automatic escalation under certain conditions
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EscalationRule {
/// Unique name for this rule
pub name: String,
/// Condition expression (simplified DSL)
pub condition: EscalationCondition,
/// Target lane to escalate to
pub target_lane: u8,
/// Optional notification channels
pub notify: Vec<String>,
/// Whether this rule is enabled
pub enabled: bool,
/// Priority (lower = higher priority)
pub priority: u32,
}
impl EscalationRule {
/// Create a new escalation rule
#[must_use]
pub fn new(name: impl Into<String>, condition: EscalationCondition, target_lane: u8) -> Self {
Self {
name: name.into(),
condition,
target_lane,
notify: Vec::new(),
enabled: true,
priority: 100,
}
}
/// Add a notification channel
#[must_use]
pub fn with_notify(mut self, channel: impl Into<String>) -> Self {
self.notify.push(channel.into());
self
}
/// Set the priority
#[must_use]
pub const fn with_priority(mut self, priority: u32) -> Self {
self.priority = priority;
self
}
/// Disable the rule
#[must_use]
pub const fn disabled(mut self) -> Self {
self.enabled = false;
self
}
}
/// Condition for triggering an escalation
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum EscalationCondition {
/// Energy exceeds threshold
EnergyAbove(f32),
/// Energy persists above threshold for duration
PersistentEnergy { threshold: f32, duration_secs: u64 },
/// Spectral drift detected
SpectralDrift { magnitude: f32 },
/// Multiple consecutive rejections
ConsecutiveRejections { count: u32 },
/// Compound condition (all must be true)
All(Vec<EscalationCondition>),
/// Compound condition (any must be true)
Any(Vec<EscalationCondition>),
}
/// Policy error types
#[derive(Debug, Error)]
pub enum PolicyError {
/// Policy is not in an editable state
#[error("Policy is not editable (status: {0:?})")]
NotEditable(PolicyBundleStatus),
/// Policy is not active
#[error("Policy is not active (status: {0:?})")]
NotActive(PolicyBundleStatus),
/// Insufficient approvals
#[error("Insufficient approvals: {current} of {required}")]
InsufficientApprovals { current: usize, required: usize },
/// Duplicate approver
#[error("Duplicate approval from: {0}")]
DuplicateApprover(ApproverId),
/// Invalid threshold configuration
#[error("Invalid threshold configuration: {0}")]
InvalidThreshold(String),
/// Scope not found
#[error("Scope not found: {0}")]
ScopeNotFound(String),
/// Policy already exists
#[error("Policy already exists: {0}")]
AlreadyExists(PolicyBundleId),
/// Content hash mismatch
#[error("Content hash mismatch")]
HashMismatch,
}
/// Versioned, signed policy bundle for threshold configuration
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PolicyBundle {
/// Unique bundle identifier
pub id: PolicyBundleId,
/// Semantic version
pub version: Version,
/// Human-readable name
pub name: String,
/// Optional description
pub description: Option<String>,
/// Current lifecycle status
pub status: PolicyBundleStatus,
/// Threshold configurations by scope pattern
pub thresholds: HashMap<String, ThresholdConfig>,
/// Escalation rules
pub escalation_rules: Vec<EscalationRule>,
/// Approvals collected
pub approvals: Vec<ApprovalSignature>,
/// Minimum required approvals for activation
pub required_approvals: usize,
/// Allowed approvers (if empty, any approver is valid)
pub allowed_approvers: Vec<ApproverId>,
/// Creation timestamp
pub created_at: Timestamp,
/// Last modification timestamp
pub updated_at: Timestamp,
/// Optional reference to superseded bundle
pub supersedes: Option<PolicyBundleId>,
/// Activation timestamp (when status became Active)
pub activated_at: Option<Timestamp>,
/// Cached content hash (recomputed on access if None)
#[serde(skip)]
cached_hash: Option<Hash>,
}
impl PolicyBundle {
/// Create a new policy bundle in Draft status
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
let now = Timestamp::now();
Self {
id: PolicyBundleId::new(),
version: Version::initial(),
name: name.into(),
description: None,
status: PolicyBundleStatus::Draft,
thresholds: HashMap::new(),
escalation_rules: Vec::new(),
approvals: Vec::new(),
required_approvals: 1,
allowed_approvers: Vec::new(),
created_at: now,
updated_at: now,
supersedes: None,
activated_at: None,
cached_hash: None,
}
}
/// Compute the content hash of this bundle
#[must_use]
pub fn content_hash(&self) -> Hash {
let mut hasher = blake3::Hasher::new();
// Hash identifying fields
hasher.update(self.id.as_bytes());
hasher.update(&self.version.major.to_le_bytes());
hasher.update(&self.version.minor.to_le_bytes());
hasher.update(&self.version.patch.to_le_bytes());
hasher.update(self.name.as_bytes());
// Hash thresholds (sorted for determinism)
let mut scope_keys: Vec<_> = self.thresholds.keys().collect();
scope_keys.sort();
for key in scope_keys {
hasher.update(key.as_bytes());
if let Some(config) = self.thresholds.get(key) {
hasher.update(&config.reflex.to_le_bytes());
hasher.update(&config.retrieval.to_le_bytes());
hasher.update(&config.heavy.to_le_bytes());
hasher.update(&config.persistence_window.as_secs().to_le_bytes());
}
}
// Hash escalation rules
for rule in &self.escalation_rules {
hasher.update(rule.name.as_bytes());
hasher.update(&rule.target_lane.to_le_bytes());
hasher.update(&rule.priority.to_le_bytes());
}
// Hash governance params
hasher.update(&self.required_approvals.to_le_bytes());
Hash::from_blake3(hasher.finalize())
}
/// Get a reference to this bundle
#[must_use]
pub fn reference(&self) -> PolicyBundleRef {
PolicyBundleRef::from_bundle(self)
}
/// Add a threshold configuration for a scope
///
/// # Errors
///
/// Returns error if policy is not editable or threshold is invalid
pub fn add_threshold(
&mut self,
scope: impl Into<String>,
config: ThresholdConfig,
) -> Result<(), PolicyError> {
if !self.status.is_editable() {
return Err(PolicyError::NotEditable(self.status));
}
if !config.is_valid() {
return Err(PolicyError::InvalidThreshold(
"Thresholds must be ordered: reflex <= retrieval <= heavy".to_string(),
));
}
self.thresholds.insert(scope.into(), config);
self.updated_at = Timestamp::now();
self.cached_hash = None;
Ok(())
}
/// Add an escalation rule
///
/// # Errors
///
/// Returns error if policy is not editable
pub fn add_escalation_rule(&mut self, rule: EscalationRule) -> Result<(), PolicyError> {
if !self.status.is_editable() {
return Err(PolicyError::NotEditable(self.status));
}
self.escalation_rules.push(rule);
self.escalation_rules.sort_by_key(|r| r.priority);
self.updated_at = Timestamp::now();
self.cached_hash = None;
Ok(())
}
/// Get threshold config for a scope (with fallback to "default")
#[must_use]
pub fn get_threshold(&self, scope: &str) -> Option<&ThresholdConfig> {
self.thresholds
.get(scope)
.or_else(|| self.thresholds.get("default"))
}
/// Set the number of required approvals
///
/// # Errors
///
/// Returns error if policy is not editable
pub fn set_required_approvals(&mut self, count: usize) -> Result<(), PolicyError> {
if !self.status.is_editable() {
return Err(PolicyError::NotEditable(self.status));
}
self.required_approvals = count;
self.updated_at = Timestamp::now();
Ok(())
}
/// Add an allowed approver
///
/// # Errors
///
/// Returns error if policy is not editable
pub fn add_allowed_approver(&mut self, approver: ApproverId) -> Result<(), PolicyError> {
if !self.status.is_editable() {
return Err(PolicyError::NotEditable(self.status));
}
if !self.allowed_approvers.contains(&approver) {
self.allowed_approvers.push(approver);
self.updated_at = Timestamp::now();
}
Ok(())
}
/// Submit the bundle for approval (Draft -> Pending)
///
/// # Errors
///
/// Returns error if not in Draft status
pub fn submit_for_approval(&mut self) -> Result<(), PolicyError> {
if self.status != PolicyBundleStatus::Draft {
return Err(PolicyError::NotEditable(self.status));
}
self.status = PolicyBundleStatus::Pending;
self.updated_at = Timestamp::now();
Ok(())
}
/// Add an approval signature
///
/// # Errors
///
/// Returns error if:
/// - Policy is not pending
/// - Approver is not allowed
/// - Approver has already signed
pub fn add_approval(&mut self, approval: ApprovalSignature) -> Result<(), PolicyError> {
if self.status != PolicyBundleStatus::Pending {
return Err(PolicyError::NotEditable(self.status));
}
// Check if approver is allowed (if list is not empty)
if !self.allowed_approvers.is_empty()
&& !self.allowed_approvers.contains(&approval.approver_id)
{
return Err(PolicyError::DuplicateApprover(approval.approver_id));
}
// Check for duplicate
if self
.approvals
.iter()
.any(|a| a.approver_id == approval.approver_id)
{
return Err(PolicyError::DuplicateApprover(approval.approver_id));
}
self.approvals.push(approval);
self.updated_at = Timestamp::now();
// Auto-activate if we have enough approvals
if self.approvals.len() >= self.required_approvals {
self.status = PolicyBundleStatus::Active;
self.activated_at = Some(Timestamp::now());
}
Ok(())
}
/// Check if the bundle has sufficient approvals
#[must_use]
pub fn has_sufficient_approvals(&self) -> bool {
self.approvals.len() >= self.required_approvals
}
/// Force activation (for testing or emergency)
///
/// # Errors
///
/// Returns error if already active or insufficient approvals
pub fn activate(&mut self) -> Result<(), PolicyError> {
if self.status == PolicyBundleStatus::Active {
return Ok(());
}
if !self.has_sufficient_approvals() {
return Err(PolicyError::InsufficientApprovals {
current: self.approvals.len(),
required: self.required_approvals,
});
}
self.status = PolicyBundleStatus::Active;
self.activated_at = Some(Timestamp::now());
self.updated_at = Timestamp::now();
Ok(())
}
/// Mark this bundle as superseded by another
///
/// # Errors
///
/// Returns error if not active
pub fn supersede(&mut self, successor_id: PolicyBundleId) -> Result<(), PolicyError> {
if self.status != PolicyBundleStatus::Active {
return Err(PolicyError::NotActive(self.status));
}
self.status = PolicyBundleStatus::Superseded;
self.updated_at = Timestamp::now();
// Note: supersedes field is on the successor, not here
Ok(())
}
/// Revoke this bundle (emergency invalidation)
///
/// # Errors
///
/// Returns error if already in terminal state
pub fn revoke(&mut self) -> Result<(), PolicyError> {
if self.status.is_terminal() {
return Err(PolicyError::NotEditable(self.status));
}
self.status = PolicyBundleStatus::Revoked;
self.updated_at = Timestamp::now();
Ok(())
}
/// Create a new version based on this bundle
#[must_use]
pub fn create_new_version(&self) -> Self {
let now = Timestamp::now();
Self {
id: PolicyBundleId::new(),
version: self.version.clone().bump_minor(),
name: self.name.clone(),
description: self.description.clone(),
status: PolicyBundleStatus::Draft,
thresholds: self.thresholds.clone(),
escalation_rules: self.escalation_rules.clone(),
approvals: Vec::new(),
required_approvals: self.required_approvals,
allowed_approvers: self.allowed_approvers.clone(),
created_at: now,
updated_at: now,
supersedes: Some(self.id),
activated_at: None,
cached_hash: None,
}
}
}
/// Builder for creating policy bundles
#[derive(Default)]
pub struct PolicyBundleBuilder {
name: Option<String>,
description: Option<String>,
thresholds: HashMap<String, ThresholdConfig>,
escalation_rules: Vec<EscalationRule>,
required_approvals: usize,
allowed_approvers: Vec<ApproverId>,
}
impl PolicyBundleBuilder {
/// Create a new builder
#[must_use]
pub fn new() -> Self {
Self {
name: None,
description: None,
thresholds: HashMap::new(),
escalation_rules: Vec::new(),
required_approvals: 1,
allowed_approvers: Vec::new(),
}
}
/// Set the policy name
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
#[must_use]
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
/// Add a threshold configuration
#[must_use]
pub fn with_threshold(mut self, scope: impl Into<String>, config: ThresholdConfig) -> Self {
self.thresholds.insert(scope.into(), config);
self
}
/// Add an escalation rule
#[must_use]
pub fn with_escalation_rule(mut self, rule: EscalationRule) -> Self {
self.escalation_rules.push(rule);
self
}
/// Set required approvals
#[must_use]
pub const fn with_required_approvals(mut self, count: usize) -> Self {
self.required_approvals = count;
self
}
/// Add an allowed approver
#[must_use]
pub fn with_approver(mut self, approver: ApproverId) -> Self {
self.allowed_approvers.push(approver);
self
}
/// Build the policy bundle
///
/// # Errors
///
/// Returns error if name is not set or thresholds are invalid
pub fn build(self) -> Result<PolicyBundle, PolicyError> {
let name = self
.name
.ok_or_else(|| PolicyError::InvalidThreshold("Policy name is required".to_string()))?;
// Validate all thresholds
for (scope, config) in &self.thresholds {
if !config.is_valid() {
return Err(PolicyError::InvalidThreshold(format!(
"Invalid threshold for scope '{scope}'"
)));
}
}
let now = Timestamp::now();
Ok(PolicyBundle {
id: PolicyBundleId::new(),
version: Version::initial(),
name,
description: self.description,
status: PolicyBundleStatus::Draft,
thresholds: self.thresholds,
escalation_rules: self.escalation_rules,
approvals: Vec::new(),
required_approvals: self.required_approvals,
allowed_approvers: self.allowed_approvers,
created_at: now,
updated_at: now,
supersedes: None,
activated_at: None,
cached_hash: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_policy_bundle_creation() {
let policy = PolicyBundle::new("test-policy");
assert_eq!(policy.name, "test-policy");
assert_eq!(policy.status, PolicyBundleStatus::Draft);
assert!(policy.status.is_editable());
}
#[test]
fn test_threshold_config_validation() {
let valid = ThresholdConfig::new(0.3, 0.6, 0.9);
assert!(valid.is_valid());
let invalid = ThresholdConfig::new(0.9, 0.6, 0.3); // Wrong order
assert!(!invalid.is_valid());
}
#[test]
fn test_policy_lifecycle() -> Result<(), PolicyError> {
let mut policy = PolicyBundle::new("test");
policy.add_threshold("default", ThresholdConfig::default())?;
policy.set_required_approvals(2)?;
// Submit for approval
policy.submit_for_approval()?;
assert_eq!(policy.status, PolicyBundleStatus::Pending);
// Add approvals
policy.add_approval(ApprovalSignature::placeholder(ApproverId::new("approver1")))?;
assert_eq!(policy.status, PolicyBundleStatus::Pending); // Still pending
policy.add_approval(ApprovalSignature::placeholder(ApproverId::new("approver2")))?;
assert_eq!(policy.status, PolicyBundleStatus::Active); // Auto-activated
Ok(())
}
#[test]
fn test_duplicate_approver_rejected() -> Result<(), PolicyError> {
let mut policy = PolicyBundle::new("test");
// Require 2 approvals so policy stays pending after first approval
policy.set_required_approvals(2)?;
policy.submit_for_approval()?;
let approver = ApproverId::new("same-approver");
policy.add_approval(ApprovalSignature::placeholder(approver.clone()))?;
// Second approval from same approver should fail
let result = policy.add_approval(ApprovalSignature::placeholder(approver));
assert!(matches!(result, Err(PolicyError::DuplicateApprover(_))));
Ok(())
}
#[test]
fn test_immutability_after_activation() -> Result<(), PolicyError> {
let mut policy = PolicyBundle::new("test");
policy.submit_for_approval()?;
policy.add_approval(ApprovalSignature::placeholder(ApproverId::new("approver")))?;
assert_eq!(policy.status, PolicyBundleStatus::Active);
// Trying to modify should fail
let result = policy.add_threshold("new-scope", ThresholdConfig::default());
assert!(matches!(result, Err(PolicyError::NotEditable(_))));
Ok(())
}
#[test]
fn test_content_hash_determinism() {
let mut policy1 = PolicyBundle::new("test");
let _ = policy1.add_threshold("scope1", ThresholdConfig::default());
let mut policy2 = PolicyBundle::new("test");
let _ = policy2.add_threshold("scope1", ThresholdConfig::default());
// Same content should produce same hash (ignoring ID)
// Note: IDs are different, so hashes will differ
// But hashing the same bundle twice should be deterministic
let hash1 = policy1.content_hash();
let hash2 = policy1.content_hash();
assert_eq!(hash1, hash2);
}
#[test]
fn test_builder() -> Result<(), PolicyError> {
let policy = PolicyBundleBuilder::new()
.name("my-policy")
.description("A test policy")
.with_threshold("default", ThresholdConfig::default())
.with_threshold("strict", ThresholdConfig::strict())
.with_required_approvals(2)
.with_approver(ApproverId::new("admin1"))
.with_approver(ApproverId::new("admin2"))
.build()?;
assert_eq!(policy.name, "my-policy");
assert_eq!(policy.thresholds.len(), 2);
assert_eq!(policy.required_approvals, 2);
assert_eq!(policy.allowed_approvers.len(), 2);
Ok(())
}
#[test]
fn test_new_version_creation() -> Result<(), PolicyError> {
let mut original = PolicyBundle::new("test");
original.add_threshold("default", ThresholdConfig::default())?;
original.submit_for_approval()?;
original.add_approval(ApprovalSignature::placeholder(ApproverId::new("approver")))?;
let new_version = original.create_new_version();
assert_ne!(new_version.id, original.id);
assert_eq!(new_version.supersedes, Some(original.id));
assert_eq!(new_version.version, Version::new(1, 1, 0));
assert_eq!(new_version.status, PolicyBundleStatus::Draft);
assert!(new_version.approvals.is_empty());
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,722 @@
//! Witness Record Entity
//!
//! Implements immutable proof of every gate decision with content hashing.
//!
//! # Witness Chain
//!
//! Each witness record references its predecessor, forming a linked chain:
//!
//! ```text
//! Witness N-2 <-- Witness N-1 <-- Witness N
//! ^ ^ ^
//! | | |
//! hash(N-2) hash(N-1) hash(N)
//! ```
//!
//! This provides:
//! - Temporal ordering guarantee
//! - Tamper detection (any modification breaks the chain)
//! - Deterministic replay capability
//!
//! # Core Invariant
//!
//! **No action without witness**: Every gate decision MUST produce a witness record.
use super::{Hash, PolicyBundleRef, Timestamp};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use uuid::Uuid;
/// Unique identifier for a witness record
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct WitnessId(pub Uuid);
impl WitnessId {
/// Generate a new random ID
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Create from a UUID
#[must_use]
pub const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
/// Get as bytes
#[must_use]
pub fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
/// Create a nil/sentinel ID
#[must_use]
pub const fn nil() -> Self {
Self(Uuid::nil())
}
/// Check if this is the nil ID
#[must_use]
pub fn is_nil(&self) -> bool {
self.0.is_nil()
}
}
impl Default for WitnessId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for WitnessId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Compute lane levels (from ADR-014)
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum ComputeLane {
/// Lane 0: Local residual updates, simple aggregates (<1ms)
Reflex = 0,
/// Lane 1: Evidence fetching, lightweight reasoning (~10ms)
Retrieval = 1,
/// Lane 2: Multi-step planning, spectral analysis (~100ms)
Heavy = 2,
/// Lane 3: Human escalation for sustained incoherence
Human = 3,
}
impl ComputeLane {
/// Get the numeric value
#[must_use]
pub const fn as_u8(&self) -> u8 {
*self as u8
}
/// Create from numeric value
#[must_use]
pub const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Reflex),
1 => Some(Self::Retrieval),
2 => Some(Self::Heavy),
3 => Some(Self::Human),
_ => None,
}
}
/// Check if this lane requires human intervention
#[must_use]
pub const fn requires_human(&self) -> bool {
matches!(self, Self::Human)
}
}
impl std::fmt::Display for ComputeLane {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Reflex => write!(f, "Reflex"),
Self::Retrieval => write!(f, "Retrieval"),
Self::Heavy => write!(f, "Heavy"),
Self::Human => write!(f, "Human"),
}
}
}
/// Gate decision result
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct GateDecision {
/// Whether the action was allowed
pub allow: bool,
/// Required compute lane
pub lane: ComputeLane,
/// Reason for the decision (especially if denied)
pub reason: Option<String>,
/// Confidence in the decision (0.0 to 1.0)
pub confidence: f32,
/// Additional decision metadata
pub metadata: HashMap<String, String>,
}
impl GateDecision {
/// Create an allow decision
#[must_use]
pub fn allow(lane: ComputeLane) -> Self {
Self {
allow: true,
lane,
reason: None,
confidence: 1.0,
metadata: HashMap::new(),
}
}
/// Create a deny decision
#[must_use]
pub fn deny(lane: ComputeLane, reason: impl Into<String>) -> Self {
Self {
allow: false,
lane,
reason: Some(reason.into()),
confidence: 1.0,
metadata: HashMap::new(),
}
}
/// Set confidence level
#[must_use]
pub const fn with_confidence(mut self, confidence: f32) -> Self {
self.confidence = confidence;
self
}
/// Add metadata
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
/// Snapshot of coherence energy at decision time
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EnergySnapshot {
/// Total system energy (lower = more coherent)
pub total_energy: f32,
/// Energy for the specific scope being evaluated
pub scope_energy: f32,
/// Scope identifier
pub scope: String,
/// Number of edges contributing to this energy
pub edge_count: u32,
/// Timestamp when energy was computed
pub computed_at: Timestamp,
/// Fingerprint for change detection
pub fingerprint: Hash,
/// Per-scope breakdown (optional)
pub scope_breakdown: Option<HashMap<String, f32>>,
}
impl EnergySnapshot {
/// Create a new energy snapshot
#[must_use]
pub fn new(total_energy: f32, scope_energy: f32, scope: impl Into<String>) -> Self {
Self {
total_energy,
scope_energy,
scope: scope.into(),
edge_count: 0,
computed_at: Timestamp::now(),
fingerprint: Hash::zero(),
scope_breakdown: None,
}
}
/// Set edge count
#[must_use]
pub const fn with_edge_count(mut self, count: u32) -> Self {
self.edge_count = count;
self
}
/// Set fingerprint
#[must_use]
pub const fn with_fingerprint(mut self, fingerprint: Hash) -> Self {
self.fingerprint = fingerprint;
self
}
/// Add scope breakdown
#[must_use]
pub fn with_breakdown(mut self, breakdown: HashMap<String, f32>) -> Self {
self.scope_breakdown = Some(breakdown);
self
}
/// Compute content hash for this snapshot
#[must_use]
pub fn content_hash(&self) -> Hash {
let mut hasher = blake3::Hasher::new();
hasher.update(&self.total_energy.to_le_bytes());
hasher.update(&self.scope_energy.to_le_bytes());
hasher.update(self.scope.as_bytes());
hasher.update(&self.edge_count.to_le_bytes());
hasher.update(&self.computed_at.secs.to_le_bytes());
hasher.update(&self.computed_at.nanos.to_le_bytes());
hasher.update(self.fingerprint.as_bytes());
Hash::from_blake3(hasher.finalize())
}
}
/// Witness chain integrity errors
#[derive(Debug, Error)]
pub enum WitnessChainError {
/// Previous witness not found
#[error("Previous witness not found: {0}")]
PreviousNotFound(WitnessId),
/// Chain hash mismatch
#[error("Chain hash mismatch at witness {0}")]
HashMismatch(WitnessId),
/// Temporal ordering violation
#[error("Temporal ordering violation: {0} should be before {1}")]
TemporalViolation(WitnessId, WitnessId),
/// Gap in sequence
#[error("Gap in witness sequence at {0}")]
SequenceGap(u64),
}
/// Witness-related errors
#[derive(Debug, Error)]
pub enum WitnessError {
/// Chain integrity error
#[error("Chain integrity error: {0}")]
ChainError(#[from] WitnessChainError),
/// Invalid witness data
#[error("Invalid witness data: {0}")]
InvalidData(String),
/// Witness not found
#[error("Witness not found: {0}")]
NotFound(WitnessId),
/// Witness already exists
#[error("Witness already exists: {0}")]
AlreadyExists(WitnessId),
}
/// Immutable proof of a gate decision
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WitnessRecord {
/// Unique witness identifier
pub id: WitnessId,
/// Sequence number within the chain
pub sequence: u64,
/// Hash of the action that was evaluated
pub action_hash: Hash,
/// Energy state at time of evaluation
pub energy_snapshot: EnergySnapshot,
/// Gate decision made
pub decision: GateDecision,
/// Policy bundle used for evaluation
pub policy_bundle_ref: PolicyBundleRef,
/// Creation timestamp
pub timestamp: Timestamp,
/// Reference to previous witness in chain (None for genesis)
pub previous_witness: Option<WitnessId>,
/// Hash of previous witness content (for chain integrity)
pub previous_hash: Option<Hash>,
/// Content hash of this witness (computed on creation)
pub content_hash: Hash,
/// Optional actor who triggered the action
pub actor: Option<String>,
/// Optional correlation ID for request tracing
pub correlation_id: Option<String>,
}
impl WitnessRecord {
/// Create a new witness record
///
/// # Arguments
///
/// * `action_hash` - Hash of the action being witnessed
/// * `energy_snapshot` - Energy state at decision time
/// * `decision` - The gate decision
/// * `policy_bundle_ref` - Reference to the policy used
/// * `previous` - Previous witness in chain (None for genesis)
#[must_use]
pub fn new(
action_hash: Hash,
energy_snapshot: EnergySnapshot,
decision: GateDecision,
policy_bundle_ref: PolicyBundleRef,
previous: Option<&WitnessRecord>,
) -> Self {
let id = WitnessId::new();
let timestamp = Timestamp::now();
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,
action_hash,
energy_snapshot,
decision,
policy_bundle_ref,
timestamp,
previous_witness,
previous_hash,
content_hash: Hash::zero(), // Placeholder, computed below
actor: None,
correlation_id: None,
};
// Compute and set content hash
witness.content_hash = witness.compute_content_hash();
witness
}
/// Create a genesis witness (first in chain)
#[must_use]
pub fn genesis(
action_hash: Hash,
energy_snapshot: EnergySnapshot,
decision: GateDecision,
policy_bundle_ref: PolicyBundleRef,
) -> Self {
Self::new(
action_hash,
energy_snapshot,
decision,
policy_bundle_ref,
None,
)
}
/// Set the actor
#[must_use]
pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
self.actor = Some(actor.into());
// Recompute hash since we changed content
self.content_hash = self.compute_content_hash();
self
}
/// Set correlation ID
#[must_use]
pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
self.correlation_id = Some(id.into());
// Recompute hash since we changed content
self.content_hash = self.compute_content_hash();
self
}
/// Compute the content hash using Blake3
#[must_use]
pub fn compute_content_hash(&self) -> Hash {
let mut hasher = blake3::Hasher::new();
// Core identifying fields
hasher.update(self.id.as_bytes());
hasher.update(&self.sequence.to_le_bytes());
hasher.update(self.action_hash.as_bytes());
// Energy snapshot hash
hasher.update(self.energy_snapshot.content_hash().as_bytes());
// Decision
hasher.update(&[self.decision.allow as u8]);
hasher.update(&[self.decision.lane.as_u8()]);
hasher.update(&self.decision.confidence.to_le_bytes());
if let Some(ref reason) = self.decision.reason {
hasher.update(reason.as_bytes());
}
// Policy reference
hasher.update(&self.policy_bundle_ref.as_bytes());
// Timestamp
hasher.update(&self.timestamp.secs.to_le_bytes());
hasher.update(&self.timestamp.nanos.to_le_bytes());
// Chain linkage
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());
}
// Optional fields
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());
}
Hash::from_blake3(hasher.finalize())
}
/// Verify the content hash is correct
#[must_use]
pub fn verify_content_hash(&self) -> bool {
self.content_hash == self.compute_content_hash()
}
/// Verify the chain linkage to a previous witness
///
/// # Errors
///
/// Returns error if:
/// - Previous witness hash doesn't match
/// - Sequence numbers are not consecutive
/// - Timestamp ordering is violated
pub fn verify_chain_link(&self, previous: &WitnessRecord) -> Result<(), WitnessChainError> {
// Check ID reference
if self.previous_witness != Some(previous.id) {
return Err(WitnessChainError::PreviousNotFound(previous.id));
}
// Check hash linkage
if self.previous_hash != Some(previous.content_hash) {
return Err(WitnessChainError::HashMismatch(self.id));
}
// Check sequence continuity
if self.sequence != previous.sequence + 1 {
return Err(WitnessChainError::SequenceGap(self.sequence));
}
// Check temporal ordering
if self.timestamp < previous.timestamp {
return Err(WitnessChainError::TemporalViolation(previous.id, self.id));
}
Ok(())
}
/// Check if this is a genesis witness
#[must_use]
pub fn is_genesis(&self) -> bool {
self.previous_witness.is_none() && self.sequence == 0
}
/// Get the decision outcome
#[must_use]
pub const fn was_allowed(&self) -> bool {
self.decision.allow
}
/// Get the compute lane
#[must_use]
pub const fn lane(&self) -> ComputeLane {
self.decision.lane
}
}
impl PartialEq for WitnessRecord {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for WitnessRecord {}
impl std::hash::Hash for WitnessRecord {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// Builder for creating witness chains
pub struct WitnessChainBuilder {
head: Option<WitnessRecord>,
policy_ref: PolicyBundleRef,
}
impl WitnessChainBuilder {
/// Create a new chain builder
#[must_use]
pub fn new(policy_ref: PolicyBundleRef) -> Self {
Self {
head: None,
policy_ref,
}
}
/// Create a new chain builder starting from an existing witness
#[must_use]
pub fn from_head(head: WitnessRecord) -> Self {
let policy_ref = head.policy_bundle_ref.clone();
Self {
head: Some(head),
policy_ref,
}
}
/// Add a witness to the chain
pub fn add_witness(
&mut self,
action_hash: Hash,
energy_snapshot: EnergySnapshot,
decision: GateDecision,
) -> &WitnessRecord {
let witness = WitnessRecord::new(
action_hash,
energy_snapshot,
decision,
self.policy_ref.clone(),
self.head.as_ref(),
);
self.head = Some(witness);
self.head.as_ref().unwrap()
}
/// Get the current head of the chain
#[must_use]
pub fn head(&self) -> Option<&WitnessRecord> {
self.head.as_ref()
}
/// Get the current sequence number
#[must_use]
pub fn current_sequence(&self) -> u64 {
self.head.as_ref().map_or(0, |w| w.sequence)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::governance::{PolicyBundleId, Version};
fn test_policy_ref() -> PolicyBundleRef {
PolicyBundleRef {
id: PolicyBundleId::new(),
version: Version::initial(),
content_hash: Hash::zero(),
}
}
fn test_energy_snapshot() -> EnergySnapshot {
EnergySnapshot::new(0.5, 0.3, "test-scope")
}
#[test]
fn test_witness_creation() {
let action_hash = Hash::from_bytes([1u8; 32]);
let energy = test_energy_snapshot();
let decision = GateDecision::allow(ComputeLane::Reflex);
let policy_ref = test_policy_ref();
let witness = WitnessRecord::genesis(action_hash, energy, decision, policy_ref);
assert!(witness.is_genesis());
assert!(witness.was_allowed());
assert_eq!(witness.lane(), ComputeLane::Reflex);
assert_eq!(witness.sequence, 0);
assert!(witness.verify_content_hash());
}
#[test]
fn test_witness_chain() {
let policy_ref = test_policy_ref();
let mut builder = WitnessChainBuilder::new(policy_ref);
// Genesis
let action1 = Hash::from_bytes([1u8; 32]);
let witness1 = builder.add_witness(
action1,
test_energy_snapshot(),
GateDecision::allow(ComputeLane::Reflex),
);
assert!(witness1.is_genesis());
let witness1_id = witness1.id.clone();
// Second witness
let action2 = Hash::from_bytes([2u8; 32]);
let witness2 = builder.add_witness(
action2,
test_energy_snapshot(),
GateDecision::deny(ComputeLane::Heavy, "High energy"),
);
assert!(!witness2.is_genesis());
assert_eq!(witness2.sequence, 1);
assert_eq!(witness2.previous_witness, Some(witness1_id));
}
#[test]
fn test_chain_verification() {
let policy_ref = test_policy_ref();
// Create genesis
let genesis = WitnessRecord::genesis(
Hash::from_bytes([1u8; 32]),
test_energy_snapshot(),
GateDecision::allow(ComputeLane::Reflex),
policy_ref.clone(),
);
// Create next witness
let next = WitnessRecord::new(
Hash::from_bytes([2u8; 32]),
test_energy_snapshot(),
GateDecision::allow(ComputeLane::Retrieval),
policy_ref,
Some(&genesis),
);
// Verify chain link
assert!(next.verify_chain_link(&genesis).is_ok());
}
#[test]
fn test_content_hash_determinism() {
let action = Hash::from_bytes([1u8; 32]);
let energy = test_energy_snapshot();
let decision = GateDecision::allow(ComputeLane::Reflex);
let policy_ref = test_policy_ref();
let witness =
WitnessRecord::genesis(action, energy.clone(), decision.clone(), policy_ref.clone());
// Verify hash is consistent
let hash1 = witness.compute_content_hash();
let hash2 = witness.compute_content_hash();
assert_eq!(hash1, hash2);
}
#[test]
fn test_tamper_detection() {
let action = Hash::from_bytes([1u8; 32]);
let energy = test_energy_snapshot();
let decision = GateDecision::allow(ComputeLane::Reflex);
let policy_ref = test_policy_ref();
let mut witness = WitnessRecord::genesis(action, energy, decision, policy_ref);
// Tamper with the witness
witness.decision.confidence = 0.5;
// Content hash should no longer match
assert!(!witness.verify_content_hash());
}
#[test]
fn test_gate_decision() {
let allow = GateDecision::allow(ComputeLane::Reflex)
.with_confidence(0.95)
.with_metadata("source", "test");
assert!(allow.allow);
assert_eq!(allow.lane, ComputeLane::Reflex);
assert!((allow.confidence - 0.95).abs() < f32::EPSILON);
assert_eq!(allow.metadata.get("source"), Some(&"test".to_string()));
let deny = GateDecision::deny(ComputeLane::Human, "High energy detected");
assert!(!deny.allow);
assert_eq!(deny.reason, Some("High energy detected".to_string()));
}
#[test]
fn test_compute_lane() {
assert_eq!(ComputeLane::from_u8(0), Some(ComputeLane::Reflex));
assert_eq!(ComputeLane::from_u8(3), Some(ComputeLane::Human));
assert_eq!(ComputeLane::from_u8(4), None);
assert!(!ComputeLane::Reflex.requires_human());
assert!(ComputeLane::Human.requires_human());
}
}