Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
599
vendor/ruvector/crates/prime-radiant/src/execution/action.rs
vendored
Normal file
599
vendor/ruvector/crates/prime-radiant/src/execution/action.rs
vendored
Normal file
@@ -0,0 +1,599 @@
|
||||
//! # Action Trait: External Side Effects with Governance
|
||||
//!
|
||||
//! Defines the Action trait for operations that produce external side effects.
|
||||
//! All actions are subject to coherence gating and produce mandatory witness records.
|
||||
//!
|
||||
//! ## Design Philosophy
|
||||
//!
|
||||
//! Actions are the boundary between the coherence engine and the external world.
|
||||
//! Every action must:
|
||||
//!
|
||||
//! 1. Declare its scope (what coherence region it affects)
|
||||
//! 2. Estimate its impact (resource cost, reversibility)
|
||||
//! 3. Be executable with a witness record
|
||||
//! 4. Support rollback when possible
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! struct UpdateUserRecord {
|
||||
//! user_id: UserId,
|
||||
//! new_data: UserData,
|
||||
//! }
|
||||
//!
|
||||
//! impl Action for UpdateUserRecord {
|
||||
//! type Output = ();
|
||||
//! type Error = DatabaseError;
|
||||
//!
|
||||
//! fn scope(&self) -> &ScopeId {
|
||||
//! &self.user_id.scope
|
||||
//! }
|
||||
//!
|
||||
//! fn impact(&self) -> ActionImpact {
|
||||
//! ActionImpact::medium()
|
||||
//! }
|
||||
//!
|
||||
//! fn execute(&self, ctx: &ExecutionContext) -> Result<Self::Output, Self::Error> {
|
||||
//! // Execute the action
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
/// Unique identifier for an action instance.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct ActionId(pub uuid::Uuid);
|
||||
|
||||
impl ActionId {
|
||||
/// Generate a new random action ID.
|
||||
pub fn new() -> Self {
|
||||
Self(uuid::Uuid::new_v4())
|
||||
}
|
||||
|
||||
/// Create from an existing UUID.
|
||||
pub fn from_uuid(uuid: uuid::Uuid) -> Self {
|
||||
Self(uuid)
|
||||
}
|
||||
|
||||
/// Get the underlying UUID.
|
||||
pub fn as_uuid(&self) -> &uuid::Uuid {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Convert to bytes for hashing.
|
||||
pub fn as_bytes(&self) -> &[u8; 16] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ActionId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ActionId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "action-{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Scope identifier for coherence energy scoping.
|
||||
///
|
||||
/// Actions affect specific regions of the coherence graph. The scope
|
||||
/// determines which subgraph's energy is relevant for gating.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct ScopeId(pub String);
|
||||
|
||||
impl ScopeId {
|
||||
/// Create a new scope ID.
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
Self(id.into())
|
||||
}
|
||||
|
||||
/// Global scope (affects entire system).
|
||||
pub fn global() -> Self {
|
||||
Self::new("__global__")
|
||||
}
|
||||
|
||||
/// Create a scoped path (e.g., "users.123.profile").
|
||||
pub fn path(parts: &[&str]) -> Self {
|
||||
Self::new(parts.join("."))
|
||||
}
|
||||
|
||||
/// Check if this is the global scope.
|
||||
pub fn is_global(&self) -> bool {
|
||||
self.0 == "__global__"
|
||||
}
|
||||
|
||||
/// Get the scope as a string slice.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Check if this scope is a parent of another.
|
||||
pub fn is_parent_of(&self, other: &ScopeId) -> bool {
|
||||
if self.is_global() {
|
||||
return true;
|
||||
}
|
||||
other.0.starts_with(&self.0) && other.0.len() > self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ScopeId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ScopeId {
|
||||
fn from(s: &str) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ScopeId {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Impact assessment for an action.
|
||||
///
|
||||
/// Used by the coherence gate to make risk-aware decisions about
|
||||
/// whether to allow, delay, or deny actions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ActionImpact {
|
||||
/// Resource cost estimate (0.0 = free, 1.0 = maximum).
|
||||
pub cost: f32,
|
||||
|
||||
/// Reversibility (0.0 = irreversible, 1.0 = fully reversible).
|
||||
pub reversibility: f32,
|
||||
|
||||
/// Blast radius (0.0 = isolated, 1.0 = system-wide).
|
||||
pub blast_radius: f32,
|
||||
|
||||
/// Latency sensitivity (0.0 = can wait, 1.0 = time-critical).
|
||||
pub latency_sensitivity: f32,
|
||||
}
|
||||
|
||||
impl ActionImpact {
|
||||
/// Create a new impact assessment.
|
||||
pub const fn new(
|
||||
cost: f32,
|
||||
reversibility: f32,
|
||||
blast_radius: f32,
|
||||
latency_sensitivity: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
cost,
|
||||
reversibility,
|
||||
blast_radius,
|
||||
latency_sensitivity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal impact (cheap, reversible, isolated).
|
||||
pub const fn minimal() -> Self {
|
||||
Self::new(0.1, 0.9, 0.1, 0.5)
|
||||
}
|
||||
|
||||
/// Low impact action.
|
||||
pub const fn low() -> Self {
|
||||
Self::new(0.2, 0.8, 0.2, 0.5)
|
||||
}
|
||||
|
||||
/// Medium impact action.
|
||||
pub const fn medium() -> Self {
|
||||
Self::new(0.5, 0.5, 0.5, 0.5)
|
||||
}
|
||||
|
||||
/// High impact action.
|
||||
pub const fn high() -> Self {
|
||||
Self::new(0.8, 0.3, 0.7, 0.7)
|
||||
}
|
||||
|
||||
/// Critical impact (expensive, irreversible, wide blast radius).
|
||||
pub const fn critical() -> Self {
|
||||
Self::new(0.95, 0.1, 0.9, 0.9)
|
||||
}
|
||||
|
||||
/// Calculate overall risk score (0.0 to 1.0).
|
||||
///
|
||||
/// Higher risk = more likely to require escalation.
|
||||
pub fn risk_score(&self) -> f32 {
|
||||
// Weighted combination favoring irreversibility and blast radius
|
||||
let weights = [0.2, 0.35, 0.3, 0.15]; // cost, reversibility(inverted), blast_radius, latency
|
||||
|
||||
let scores = [
|
||||
self.cost,
|
||||
1.0 - self.reversibility, // Invert: low reversibility = high risk
|
||||
self.blast_radius,
|
||||
self.latency_sensitivity,
|
||||
];
|
||||
|
||||
scores.iter().zip(weights.iter()).map(|(s, w)| s * w).sum()
|
||||
}
|
||||
|
||||
/// Whether this action should be considered high-risk.
|
||||
pub fn is_high_risk(&self) -> bool {
|
||||
self.risk_score() > 0.6
|
||||
}
|
||||
|
||||
/// Whether this action is reversible enough for automatic retry.
|
||||
pub fn allows_retry(&self) -> bool {
|
||||
self.reversibility > 0.5
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ActionImpact {
|
||||
fn default() -> Self {
|
||||
Self::medium()
|
||||
}
|
||||
}
|
||||
|
||||
/// Action metadata for governance and audit.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ActionMetadata {
|
||||
/// Unique action identifier.
|
||||
pub id: ActionId,
|
||||
|
||||
/// Action type name.
|
||||
pub action_type: String,
|
||||
|
||||
/// Human-readable description.
|
||||
pub description: String,
|
||||
|
||||
/// Actor who initiated the action.
|
||||
pub actor_id: String,
|
||||
|
||||
/// Timestamp when action was created (Unix millis).
|
||||
pub created_at_ms: u64,
|
||||
|
||||
/// Optional tags for categorization.
|
||||
pub tags: Vec<String>,
|
||||
|
||||
/// Optional correlation ID for tracing.
|
||||
pub correlation_id: Option<String>,
|
||||
}
|
||||
|
||||
impl ActionMetadata {
|
||||
/// Create new metadata with required fields.
|
||||
pub fn new(
|
||||
action_type: impl Into<String>,
|
||||
description: impl Into<String>,
|
||||
actor_id: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: ActionId::new(),
|
||||
action_type: action_type.into(),
|
||||
description: description.into(),
|
||||
actor_id: actor_id.into(),
|
||||
created_at_ms: Self::current_timestamp_ms(),
|
||||
tags: Vec::new(),
|
||||
correlation_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a tag to the metadata.
|
||||
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
|
||||
self.tags.push(tag.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple tags.
|
||||
pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
|
||||
self.tags.extend(tags.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set correlation ID.
|
||||
pub fn with_correlation_id(mut self, id: impl Into<String>) -> Self {
|
||||
self.correlation_id = Some(id.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn current_timestamp_ms() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Execution context provided to actions during execution.
|
||||
///
|
||||
/// Contains references to system resources and the witness record being built.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutionContext {
|
||||
/// The action's unique ID.
|
||||
pub action_id: ActionId,
|
||||
|
||||
/// Current coherence energy for the action's scope.
|
||||
pub current_energy: f32,
|
||||
|
||||
/// The compute lane assigned for this execution.
|
||||
pub assigned_lane: super::ladder::ComputeLane,
|
||||
|
||||
/// Whether this is a retry attempt.
|
||||
pub is_retry: bool,
|
||||
|
||||
/// Retry attempt number (0 for first attempt).
|
||||
pub retry_count: u32,
|
||||
|
||||
/// Maximum allowed execution time in milliseconds.
|
||||
pub timeout_ms: u64,
|
||||
}
|
||||
|
||||
impl ExecutionContext {
|
||||
/// Create a new execution context.
|
||||
pub fn new(
|
||||
action_id: ActionId,
|
||||
current_energy: f32,
|
||||
assigned_lane: super::ladder::ComputeLane,
|
||||
) -> Self {
|
||||
Self {
|
||||
action_id,
|
||||
current_energy,
|
||||
assigned_lane,
|
||||
is_retry: false,
|
||||
retry_count: 0,
|
||||
timeout_ms: assigned_lane.latency_budget_us() / 1000,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a retry context from an existing context.
|
||||
pub fn retry(previous: &Self) -> Self {
|
||||
Self {
|
||||
action_id: previous.action_id.clone(),
|
||||
current_energy: previous.current_energy,
|
||||
assigned_lane: previous.assigned_lane,
|
||||
is_retry: true,
|
||||
retry_count: previous.retry_count + 1,
|
||||
timeout_ms: previous.timeout_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if we've exceeded the retry limit.
|
||||
pub fn exceeded_retries(&self, max_retries: u32) -> bool {
|
||||
self.retry_count >= max_retries
|
||||
}
|
||||
}
|
||||
|
||||
/// The core Action trait for all external side effects.
|
||||
///
|
||||
/// Actions are the fundamental unit of work in the coherence engine.
|
||||
/// They represent operations that modify external state and must be
|
||||
/// governed by coherence gating.
|
||||
pub trait Action: Send + Sync {
|
||||
/// The successful output type of the action.
|
||||
type Output: Send;
|
||||
|
||||
/// The error type that can occur during execution.
|
||||
type Error: std::error::Error + Send + 'static;
|
||||
|
||||
/// Get the scope this action affects.
|
||||
///
|
||||
/// The scope determines which region of the coherence graph
|
||||
/// is consulted for gating decisions.
|
||||
fn scope(&self) -> &ScopeId;
|
||||
|
||||
/// Assess the impact of this action.
|
||||
///
|
||||
/// Used for risk-based gating decisions.
|
||||
fn impact(&self) -> ActionImpact;
|
||||
|
||||
/// Get metadata for this action.
|
||||
fn metadata(&self) -> &ActionMetadata;
|
||||
|
||||
/// Execute the action within the given context.
|
||||
///
|
||||
/// This method performs the actual side effect. It should:
|
||||
/// - Check the context for retry status
|
||||
/// - Respect the timeout
|
||||
/// - Return a meaningful error on failure
|
||||
fn execute(&self, ctx: &ExecutionContext) -> Result<Self::Output, Self::Error>;
|
||||
|
||||
/// Compute a content hash for witness records.
|
||||
///
|
||||
/// This should include all relevant action parameters.
|
||||
fn content_hash(&self) -> [u8; 32];
|
||||
|
||||
/// Whether this action supports rollback.
|
||||
fn supports_rollback(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Attempt to rollback this action.
|
||||
///
|
||||
/// Only called if `supports_rollback()` returns true.
|
||||
fn rollback(&self, _ctx: &ExecutionContext, _output: &Self::Output) -> Result<(), Self::Error> {
|
||||
Err(Self::make_rollback_not_supported_error())
|
||||
}
|
||||
|
||||
/// Create an error indicating rollback is not supported.
|
||||
///
|
||||
/// Implementations should override this to return an appropriate error type.
|
||||
fn make_rollback_not_supported_error() -> Self::Error;
|
||||
}
|
||||
|
||||
/// A boxed action that erases the output/error types.
|
||||
///
|
||||
/// Useful for storing heterogeneous actions in queues.
|
||||
pub type BoxedAction = Box<dyn Action<Output = (), Error = ActionError> + Send + Sync>;
|
||||
|
||||
/// Generic action error for boxed actions.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ActionError {
|
||||
#[error("Action execution failed: {0}")]
|
||||
ExecutionFailed(String),
|
||||
|
||||
#[error("Action timed out after {0}ms")]
|
||||
Timeout(u64),
|
||||
|
||||
#[error("Action was denied by coherence gate: {0}")]
|
||||
Denied(String),
|
||||
|
||||
#[error("Rollback not supported")]
|
||||
RollbackNotSupported,
|
||||
|
||||
#[error("Rollback failed: {0}")]
|
||||
RollbackFailed(String),
|
||||
|
||||
#[error("Invalid action state: {0}")]
|
||||
InvalidState(String),
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Result of an action execution attempt.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ActionResult {
|
||||
/// The action ID.
|
||||
pub action_id: ActionId,
|
||||
|
||||
/// Whether execution succeeded.
|
||||
pub success: bool,
|
||||
|
||||
/// Error message if failed.
|
||||
pub error_message: Option<String>,
|
||||
|
||||
/// Execution duration in microseconds.
|
||||
pub duration_us: u64,
|
||||
|
||||
/// The compute lane used.
|
||||
pub lane: super::ladder::ComputeLane,
|
||||
|
||||
/// Retry count.
|
||||
pub retry_count: u32,
|
||||
|
||||
/// Timestamp of completion (Unix millis).
|
||||
pub completed_at_ms: u64,
|
||||
}
|
||||
|
||||
impl ActionResult {
|
||||
/// Create a successful result.
|
||||
pub fn success(
|
||||
action_id: ActionId,
|
||||
duration_us: u64,
|
||||
lane: super::ladder::ComputeLane,
|
||||
retry_count: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
action_id,
|
||||
success: true,
|
||||
error_message: None,
|
||||
duration_us,
|
||||
lane,
|
||||
retry_count,
|
||||
completed_at_ms: Self::current_timestamp_ms(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a failure result.
|
||||
pub fn failure(
|
||||
action_id: ActionId,
|
||||
error: impl fmt::Display,
|
||||
duration_us: u64,
|
||||
lane: super::ladder::ComputeLane,
|
||||
retry_count: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
action_id,
|
||||
success: false,
|
||||
error_message: Some(error.to_string()),
|
||||
duration_us,
|
||||
lane,
|
||||
retry_count,
|
||||
completed_at_ms: Self::current_timestamp_ms(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_timestamp_ms() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_action_id() {
|
||||
let id1 = ActionId::new();
|
||||
let id2 = ActionId::new();
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_id() {
|
||||
let global = ScopeId::global();
|
||||
assert!(global.is_global());
|
||||
|
||||
let user_scope = ScopeId::path(&["users", "123"]);
|
||||
assert!(!user_scope.is_global());
|
||||
assert_eq!(user_scope.as_str(), "users.123");
|
||||
|
||||
let parent = ScopeId::new("users");
|
||||
assert!(parent.is_parent_of(&user_scope));
|
||||
assert!(global.is_parent_of(&user_scope));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_impact() {
|
||||
let minimal = ActionImpact::minimal();
|
||||
let critical = ActionImpact::critical();
|
||||
|
||||
assert!(minimal.risk_score() < critical.risk_score());
|
||||
assert!(!minimal.is_high_risk());
|
||||
assert!(critical.is_high_risk());
|
||||
assert!(minimal.allows_retry());
|
||||
assert!(!critical.allows_retry());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_execution_context_retry() {
|
||||
let ctx = ExecutionContext::new(
|
||||
ActionId::new(),
|
||||
0.5,
|
||||
super::super::ladder::ComputeLane::Reflex,
|
||||
);
|
||||
|
||||
assert!(!ctx.is_retry);
|
||||
assert_eq!(ctx.retry_count, 0);
|
||||
|
||||
let retry_ctx = ExecutionContext::retry(&ctx);
|
||||
assert!(retry_ctx.is_retry);
|
||||
assert_eq!(retry_ctx.retry_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_result() {
|
||||
let action_id = ActionId::new();
|
||||
|
||||
let success = ActionResult::success(
|
||||
action_id.clone(),
|
||||
500,
|
||||
super::super::ladder::ComputeLane::Reflex,
|
||||
0,
|
||||
);
|
||||
assert!(success.success);
|
||||
assert!(success.error_message.is_none());
|
||||
|
||||
let failure = ActionResult::failure(
|
||||
action_id,
|
||||
"Something went wrong",
|
||||
1000,
|
||||
super::super::ladder::ComputeLane::Retrieval,
|
||||
1,
|
||||
);
|
||||
assert!(!failure.success);
|
||||
assert!(failure.error_message.is_some());
|
||||
}
|
||||
}
|
||||
860
vendor/ruvector/crates/prime-radiant/src/execution/executor.rs
vendored
Normal file
860
vendor/ruvector/crates/prime-radiant/src/execution/executor.rs
vendored
Normal file
@@ -0,0 +1,860 @@
|
||||
//! # Action Executor: Mandatory Witness Creation
|
||||
//!
|
||||
//! The executor is responsible for running actions through the coherence gate
|
||||
//! and ensuring every execution produces a witness record.
|
||||
//!
|
||||
//! ## Design Principle
|
||||
//!
|
||||
//! > All decisions and external side effects produce mandatory witness and
|
||||
//! > lineage records, making every action auditable and replayable.
|
||||
//!
|
||||
//! ## Execution Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! Action Submitted
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────┐
|
||||
//! │ Gate Evaluation │ → Witness Created (MANDATORY)
|
||||
//! └─────────────────┘
|
||||
//! │
|
||||
//! ├── Denied ──────────────────────┐
|
||||
//! │ ▼
|
||||
//! │ Return Denial + Witness
|
||||
//! │
|
||||
//! ├── Human Lane ──────────────────┐
|
||||
//! │ ▼
|
||||
//! │ Queue for Human Review
|
||||
//! │
|
||||
//! └── Allowed ─────────────────────┐
|
||||
//! ▼
|
||||
//! ┌─────────────────┐
|
||||
//! │ Execute Action │
|
||||
//! └─────────────────┘
|
||||
//! │
|
||||
//! ├── Success ──┐
|
||||
//! │ ▼
|
||||
//! │ Return Success + Witness
|
||||
//! │
|
||||
//! └── Failure ──┐
|
||||
//! ▼
|
||||
//! Retry or Return Error
|
||||
//! ```
|
||||
|
||||
use super::action::{Action, ActionError, ActionId, ActionResult, ExecutionContext};
|
||||
use super::gate::{CoherenceGate, EnergySnapshot, GateDecision, WitnessRecord};
|
||||
use super::ladder::ComputeLane;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Configuration for the action executor.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutorConfig {
|
||||
/// Maximum retry attempts for failed actions.
|
||||
pub max_retries: u32,
|
||||
|
||||
/// Base delay between retries (with exponential backoff).
|
||||
pub retry_delay: Duration,
|
||||
|
||||
/// Maximum delay between retries.
|
||||
pub max_retry_delay: Duration,
|
||||
|
||||
/// Maximum pending human review queue size.
|
||||
pub max_human_queue: usize,
|
||||
|
||||
/// Whether to store all witnesses (vs. only failures/escalations).
|
||||
pub store_all_witnesses: bool,
|
||||
|
||||
/// Maximum witnesses to keep in memory.
|
||||
pub max_witnesses_in_memory: usize,
|
||||
}
|
||||
|
||||
impl Default for ExecutorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_retries: 3,
|
||||
retry_delay: Duration::from_millis(100),
|
||||
max_retry_delay: Duration::from_secs(5),
|
||||
max_human_queue: 1000,
|
||||
store_all_witnesses: true,
|
||||
max_witnesses_in_memory: 10000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about executor operation.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ExecutorStats {
|
||||
/// Total actions submitted.
|
||||
pub total_submitted: u64,
|
||||
|
||||
/// Actions allowed through gate.
|
||||
pub total_allowed: u64,
|
||||
|
||||
/// Actions denied by gate.
|
||||
pub total_denied: u64,
|
||||
|
||||
/// Actions escalated.
|
||||
pub total_escalated: u64,
|
||||
|
||||
/// Actions executed successfully.
|
||||
pub total_success: u64,
|
||||
|
||||
/// Actions that failed execution.
|
||||
pub total_failed: u64,
|
||||
|
||||
/// Actions in human review queue.
|
||||
pub pending_human_review: usize,
|
||||
|
||||
/// Total witnesses created.
|
||||
pub witnesses_created: u64,
|
||||
|
||||
/// Actions by lane count.
|
||||
pub by_lane: [u64; 4],
|
||||
}
|
||||
|
||||
impl ExecutorStats {
|
||||
/// Get the success rate (0.0 to 1.0).
|
||||
pub fn success_rate(&self) -> f64 {
|
||||
if self.total_allowed == 0 {
|
||||
return 1.0;
|
||||
}
|
||||
self.total_success as f64 / self.total_allowed as f64
|
||||
}
|
||||
|
||||
/// Get the denial rate (0.0 to 1.0).
|
||||
pub fn denial_rate(&self) -> f64 {
|
||||
if self.total_submitted == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.total_denied as f64 / self.total_submitted as f64
|
||||
}
|
||||
|
||||
/// Get the escalation rate (0.0 to 1.0).
|
||||
pub fn escalation_rate(&self) -> f64 {
|
||||
if self.total_submitted == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.total_escalated as f64 / self.total_submitted as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Item in the human review queue.
|
||||
#[derive(Debug)]
|
||||
pub struct HumanReviewItem {
|
||||
/// The action ID awaiting review.
|
||||
pub action_id: ActionId,
|
||||
|
||||
/// The witness record for the gate decision.
|
||||
pub witness: WitnessRecord,
|
||||
|
||||
/// When this was queued.
|
||||
pub queued_at: Instant,
|
||||
|
||||
/// Energy snapshot at queue time.
|
||||
pub energy_snapshot: EnergySnapshot,
|
||||
}
|
||||
|
||||
/// Result of an execution attempt.
|
||||
#[derive(Debug)]
|
||||
pub struct ExecutionResult<T> {
|
||||
/// The action result.
|
||||
pub result: Result<T, ActionError>,
|
||||
|
||||
/// The witness record (ALWAYS present).
|
||||
pub witness: WitnessRecord,
|
||||
|
||||
/// The gate decision.
|
||||
pub decision: GateDecision,
|
||||
|
||||
/// Execution statistics.
|
||||
pub stats: ExecutionStats,
|
||||
}
|
||||
|
||||
/// Statistics for a single execution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutionStats {
|
||||
/// Time spent in gate evaluation.
|
||||
pub gate_time_us: u64,
|
||||
|
||||
/// Time spent in actual execution.
|
||||
pub execution_time_us: u64,
|
||||
|
||||
/// Total time including overhead.
|
||||
pub total_time_us: u64,
|
||||
|
||||
/// Number of retry attempts.
|
||||
pub retry_count: u32,
|
||||
|
||||
/// The lane used for execution.
|
||||
pub lane: ComputeLane,
|
||||
}
|
||||
|
||||
/// The action executor with mandatory witness creation.
|
||||
///
|
||||
/// This is the primary interface for executing actions in the coherence engine.
|
||||
/// Every execution attempt produces a witness record, regardless of success or failure.
|
||||
pub struct ActionExecutor {
|
||||
/// The coherence gate for decision making.
|
||||
gate: Arc<RwLock<CoherenceGate>>,
|
||||
|
||||
/// Configuration.
|
||||
config: ExecutorConfig,
|
||||
|
||||
/// Statistics (thread-safe).
|
||||
stats: Arc<RwLock<ExecutorStats>>,
|
||||
|
||||
/// Witness storage (in-memory ring buffer).
|
||||
witnesses: Arc<RwLock<VecDeque<WitnessRecord>>>,
|
||||
|
||||
/// Human review queue.
|
||||
human_queue: Arc<RwLock<VecDeque<HumanReviewItem>>>,
|
||||
}
|
||||
|
||||
impl ActionExecutor {
|
||||
/// Create a new action executor.
|
||||
pub fn new(gate: CoherenceGate, config: ExecutorConfig) -> Self {
|
||||
Self {
|
||||
gate: Arc::new(RwLock::new(gate)),
|
||||
config,
|
||||
stats: Arc::new(RwLock::new(ExecutorStats::default())),
|
||||
witnesses: Arc::new(RwLock::new(VecDeque::new())),
|
||||
human_queue: Arc::new(RwLock::new(VecDeque::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with default configuration.
|
||||
pub fn with_defaults(gate: CoherenceGate) -> Self {
|
||||
Self::new(gate, ExecutorConfig::default())
|
||||
}
|
||||
|
||||
/// Execute an action with mandatory witness creation.
|
||||
///
|
||||
/// This is the main entry point for action execution. It:
|
||||
/// 1. Evaluates the action through the coherence gate
|
||||
/// 2. Creates a witness record (MANDATORY)
|
||||
/// 3. Executes the action if allowed
|
||||
/// 4. Returns both the result and the witness
|
||||
pub fn execute<A: Action>(
|
||||
&self,
|
||||
action: &A,
|
||||
energy: &EnergySnapshot,
|
||||
) -> ExecutionResult<A::Output> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Update stats
|
||||
{
|
||||
let mut stats = self.stats.write();
|
||||
stats.total_submitted += 1;
|
||||
}
|
||||
|
||||
// Gate evaluation with witness creation
|
||||
let gate_start = Instant::now();
|
||||
let (decision, witness) = {
|
||||
let mut gate = self.gate.write();
|
||||
gate.evaluate_with_witness(action, energy)
|
||||
};
|
||||
let gate_time_us = gate_start.elapsed().as_micros() as u64;
|
||||
|
||||
// Extract lane before any potential moves
|
||||
let lane = decision.lane;
|
||||
let is_escalated = decision.is_escalated();
|
||||
let allow = decision.allow;
|
||||
|
||||
// Store witness
|
||||
self.store_witness(&witness);
|
||||
|
||||
// Update lane stats
|
||||
{
|
||||
let mut stats = self.stats.write();
|
||||
stats.witnesses_created += 1;
|
||||
stats.by_lane[lane.as_u8() as usize] += 1;
|
||||
|
||||
if is_escalated {
|
||||
stats.total_escalated += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle decision
|
||||
if !allow {
|
||||
debug!(
|
||||
action_id = %action.metadata().id,
|
||||
lane = ?lane,
|
||||
reason = decision.reason.as_deref().unwrap_or("unknown"),
|
||||
"Action denied by coherence gate"
|
||||
);
|
||||
|
||||
let mut stats = self.stats.write();
|
||||
stats.total_denied += 1;
|
||||
|
||||
let reason = decision
|
||||
.reason
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Gate denied".to_string());
|
||||
|
||||
return ExecutionResult {
|
||||
result: Err(ActionError::Denied(reason)),
|
||||
witness,
|
||||
decision,
|
||||
stats: ExecutionStats {
|
||||
gate_time_us,
|
||||
execution_time_us: 0,
|
||||
total_time_us: start_time.elapsed().as_micros() as u64,
|
||||
retry_count: 0,
|
||||
lane,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Handle human review lane
|
||||
if lane == ComputeLane::Human {
|
||||
info!(
|
||||
action_id = %action.metadata().id,
|
||||
"Action queued for human review"
|
||||
);
|
||||
|
||||
self.queue_for_human_review(
|
||||
action.metadata().id.clone(),
|
||||
witness.clone(),
|
||||
energy.clone(),
|
||||
);
|
||||
|
||||
let mut stats = self.stats.write();
|
||||
stats.total_allowed += 1;
|
||||
|
||||
return ExecutionResult {
|
||||
result: Err(ActionError::Denied("Queued for human review".to_string())),
|
||||
witness,
|
||||
decision,
|
||||
stats: ExecutionStats {
|
||||
gate_time_us,
|
||||
execution_time_us: 0,
|
||||
total_time_us: start_time.elapsed().as_micros() as u64,
|
||||
retry_count: 0,
|
||||
lane,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Execute with retries
|
||||
let mut stats = self.stats.write();
|
||||
stats.total_allowed += 1;
|
||||
drop(stats);
|
||||
|
||||
let execution_start = Instant::now();
|
||||
let (result, retry_count) = self.execute_with_retries(action, &decision, energy);
|
||||
let execution_time_us = execution_start.elapsed().as_micros() as u64;
|
||||
|
||||
// Update success/failure stats
|
||||
{
|
||||
let mut stats = self.stats.write();
|
||||
if result.is_ok() {
|
||||
stats.total_success += 1;
|
||||
} else {
|
||||
stats.total_failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
ExecutionResult {
|
||||
result,
|
||||
witness,
|
||||
decision,
|
||||
stats: ExecutionStats {
|
||||
gate_time_us,
|
||||
execution_time_us,
|
||||
total_time_us: start_time.elapsed().as_micros() as u64,
|
||||
retry_count,
|
||||
lane,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute action with retry logic.
|
||||
fn execute_with_retries<A: Action>(
|
||||
&self,
|
||||
action: &A,
|
||||
decision: &GateDecision,
|
||||
energy: &EnergySnapshot,
|
||||
) -> (Result<A::Output, ActionError>, u32) {
|
||||
let mut ctx = ExecutionContext::new(
|
||||
action.metadata().id.clone(),
|
||||
energy.scope_energy,
|
||||
decision.lane,
|
||||
);
|
||||
|
||||
let mut last_error_str: Option<String> = None;
|
||||
let mut delay = self.config.retry_delay;
|
||||
|
||||
for attempt in 0..=self.config.max_retries {
|
||||
if attempt > 0 {
|
||||
ctx = ExecutionContext::retry(&ctx);
|
||||
|
||||
// Exponential backoff
|
||||
std::thread::sleep(delay);
|
||||
delay = (delay * 2).min(self.config.max_retry_delay);
|
||||
|
||||
debug!(
|
||||
action_id = %action.metadata().id,
|
||||
attempt = attempt,
|
||||
"Retrying action execution"
|
||||
);
|
||||
}
|
||||
|
||||
match action.execute(&ctx) {
|
||||
Ok(output) => {
|
||||
if attempt > 0 {
|
||||
info!(
|
||||
action_id = %action.metadata().id,
|
||||
attempts = attempt + 1,
|
||||
"Action succeeded after retries"
|
||||
);
|
||||
}
|
||||
return (Ok(output), attempt);
|
||||
}
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
warn!(
|
||||
action_id = %action.metadata().id,
|
||||
attempt = attempt,
|
||||
error = %err_str,
|
||||
"Action execution failed"
|
||||
);
|
||||
last_error_str = Some(err_str);
|
||||
|
||||
// Check if action supports retry
|
||||
if !action.impact().allows_retry() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let error_msg = last_error_str.unwrap_or_else(|| "Unknown error".to_string());
|
||||
|
||||
error!(
|
||||
action_id = %action.metadata().id,
|
||||
max_retries = self.config.max_retries,
|
||||
error = %error_msg,
|
||||
"Action failed after all retries"
|
||||
);
|
||||
|
||||
(
|
||||
Err(ActionError::ExecutionFailed(format!(
|
||||
"Failed after {} retries: {}",
|
||||
self.config.max_retries, error_msg
|
||||
))),
|
||||
self.config.max_retries,
|
||||
)
|
||||
}
|
||||
|
||||
/// Store a witness record.
|
||||
fn store_witness(&self, witness: &WitnessRecord) {
|
||||
if !self.config.store_all_witnesses
|
||||
&& witness.decision.allow
|
||||
&& !witness.decision.is_escalated()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut witnesses = self.witnesses.write();
|
||||
witnesses.push_back(witness.clone());
|
||||
|
||||
// Trim old witnesses
|
||||
while witnesses.len() > self.config.max_witnesses_in_memory {
|
||||
witnesses.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue an action for human review.
|
||||
fn queue_for_human_review(
|
||||
&self,
|
||||
action_id: ActionId,
|
||||
witness: WitnessRecord,
|
||||
energy: EnergySnapshot,
|
||||
) {
|
||||
let mut queue = self.human_queue.write();
|
||||
|
||||
if queue.len() >= self.config.max_human_queue {
|
||||
warn!("Human review queue full, dropping oldest item");
|
||||
queue.pop_front();
|
||||
}
|
||||
|
||||
queue.push_back(HumanReviewItem {
|
||||
action_id,
|
||||
witness,
|
||||
queued_at: Instant::now(),
|
||||
energy_snapshot: energy,
|
||||
});
|
||||
|
||||
let mut stats = self.stats.write();
|
||||
stats.pending_human_review = queue.len();
|
||||
}
|
||||
|
||||
/// Get the next item from the human review queue.
|
||||
pub fn pop_human_review(&self) -> Option<HumanReviewItem> {
|
||||
let mut queue = self.human_queue.write();
|
||||
let item = queue.pop_front();
|
||||
|
||||
if item.is_some() {
|
||||
let mut stats = self.stats.write();
|
||||
stats.pending_human_review = queue.len();
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
/// Peek at the human review queue without removing.
|
||||
pub fn peek_human_review(&self) -> Option<HumanReviewItem> {
|
||||
let queue = self.human_queue.read();
|
||||
queue.front().map(|item| HumanReviewItem {
|
||||
action_id: item.action_id.clone(),
|
||||
witness: item.witness.clone(),
|
||||
queued_at: item.queued_at,
|
||||
energy_snapshot: item.energy_snapshot.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get current executor statistics.
|
||||
pub fn stats(&self) -> ExecutorStats {
|
||||
self.stats.read().clone()
|
||||
}
|
||||
|
||||
/// Get recent witnesses.
|
||||
pub fn recent_witnesses(&self, limit: usize) -> Vec<WitnessRecord> {
|
||||
let witnesses = self.witnesses.read();
|
||||
witnesses.iter().rev().take(limit).cloned().collect()
|
||||
}
|
||||
|
||||
/// Get a witness by ID.
|
||||
pub fn get_witness(&self, id: &super::gate::WitnessId) -> Option<WitnessRecord> {
|
||||
let witnesses = self.witnesses.read();
|
||||
witnesses.iter().find(|w| w.id == *id).cloned()
|
||||
}
|
||||
|
||||
/// Get access to the gate for configuration updates.
|
||||
pub fn gate(&self) -> Arc<RwLock<CoherenceGate>> {
|
||||
self.gate.clone()
|
||||
}
|
||||
|
||||
/// Reset executor state (for testing).
|
||||
pub fn reset(&self) {
|
||||
{
|
||||
let mut gate = self.gate.write();
|
||||
gate.reset();
|
||||
}
|
||||
{
|
||||
let mut stats = self.stats.write();
|
||||
*stats = ExecutorStats::default();
|
||||
}
|
||||
{
|
||||
let mut witnesses = self.witnesses.write();
|
||||
witnesses.clear();
|
||||
}
|
||||
{
|
||||
let mut queue = self.human_queue.write();
|
||||
queue.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ActionExecutor {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
gate: self.gate.clone(),
|
||||
config: self.config.clone(),
|
||||
stats: self.stats.clone(),
|
||||
witnesses: self.witnesses.clone(),
|
||||
human_queue: self.human_queue.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for creating a configured action result.
|
||||
pub struct ActionResultBuilder {
|
||||
action_id: ActionId,
|
||||
success: bool,
|
||||
error_message: Option<String>,
|
||||
duration_us: u64,
|
||||
lane: ComputeLane,
|
||||
retry_count: u32,
|
||||
}
|
||||
|
||||
impl ActionResultBuilder {
|
||||
/// Create a new builder.
|
||||
pub fn new(action_id: ActionId, lane: ComputeLane) -> Self {
|
||||
Self {
|
||||
action_id,
|
||||
success: true,
|
||||
error_message: None,
|
||||
duration_us: 0,
|
||||
lane,
|
||||
retry_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark as failed.
|
||||
pub fn failed(mut self, message: impl Into<String>) -> Self {
|
||||
self.success = false;
|
||||
self.error_message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set duration.
|
||||
pub fn duration_us(mut self, us: u64) -> Self {
|
||||
self.duration_us = us;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set retry count.
|
||||
pub fn retries(mut self, count: u32) -> Self {
|
||||
self.retry_count = count;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the result.
|
||||
pub fn build(self) -> ActionResult {
|
||||
if self.success {
|
||||
ActionResult::success(
|
||||
self.action_id,
|
||||
self.duration_us,
|
||||
self.lane,
|
||||
self.retry_count,
|
||||
)
|
||||
} else {
|
||||
ActionResult::failure(
|
||||
self.action_id,
|
||||
self.error_message.unwrap_or_default(),
|
||||
self.duration_us,
|
||||
self.lane,
|
||||
self.retry_count,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::execution::action::{ActionImpact, ActionMetadata, ScopeId};
|
||||
use crate::execution::gate::PolicyBundleRef;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
// Test action that tracks execution
|
||||
struct TrackedAction {
|
||||
scope: ScopeId,
|
||||
metadata: ActionMetadata,
|
||||
execute_count: Arc<AtomicU32>,
|
||||
should_fail: bool,
|
||||
}
|
||||
|
||||
impl TrackedAction {
|
||||
fn new(scope: &str) -> Self {
|
||||
Self {
|
||||
scope: ScopeId::new(scope),
|
||||
metadata: ActionMetadata::new("TrackedAction", "Test action", "test-actor"),
|
||||
execute_count: Arc::new(AtomicU32::new(0)),
|
||||
should_fail: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn failing(scope: &str) -> Self {
|
||||
Self {
|
||||
scope: ScopeId::new(scope),
|
||||
metadata: ActionMetadata::new("TrackedAction", "Failing action", "test-actor"),
|
||||
execute_count: Arc::new(AtomicU32::new(0)),
|
||||
should_fail: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn execution_count(&self) -> u32 {
|
||||
self.execute_count.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for TrackedAction {
|
||||
type Output = ();
|
||||
type Error = ActionError;
|
||||
|
||||
fn scope(&self) -> &ScopeId {
|
||||
&self.scope
|
||||
}
|
||||
|
||||
fn impact(&self) -> ActionImpact {
|
||||
ActionImpact::low()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &ActionMetadata {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
fn execute(&self, _ctx: &ExecutionContext) -> Result<(), ActionError> {
|
||||
self.execute_count.fetch_add(1, Ordering::SeqCst);
|
||||
if self.should_fail {
|
||||
Err(ActionError::ExecutionFailed(
|
||||
"Simulated failure".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn content_hash(&self) -> [u8; 32] {
|
||||
let hash = blake3::hash(self.scope.as_str().as_bytes());
|
||||
let mut result = [0u8; 32];
|
||||
result.copy_from_slice(hash.as_bytes());
|
||||
result
|
||||
}
|
||||
|
||||
fn make_rollback_not_supported_error() -> ActionError {
|
||||
ActionError::RollbackNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_success() {
|
||||
let gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
let action = TrackedAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
|
||||
let result = executor.execute(&action, &energy);
|
||||
|
||||
assert!(result.result.is_ok());
|
||||
assert!(result.decision.allow);
|
||||
assert_eq!(result.decision.lane, ComputeLane::Reflex);
|
||||
assert!(result.witness.verify_integrity());
|
||||
assert_eq!(action.execution_count(), 1);
|
||||
|
||||
let stats = executor.stats();
|
||||
assert_eq!(stats.total_submitted, 1);
|
||||
assert_eq!(stats.total_allowed, 1);
|
||||
assert_eq!(stats.total_success, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_denial() {
|
||||
let gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
let action = TrackedAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.95, 0.9, action.scope.clone());
|
||||
|
||||
let result = executor.execute(&action, &energy);
|
||||
|
||||
assert!(result.result.is_err());
|
||||
assert!(!result.decision.allow);
|
||||
assert_eq!(result.decision.lane, ComputeLane::Human);
|
||||
assert_eq!(action.execution_count(), 0); // Never executed
|
||||
|
||||
let stats = executor.stats();
|
||||
assert_eq!(stats.total_denied, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_retry() {
|
||||
let gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let mut config = ExecutorConfig::default();
|
||||
config.max_retries = 2;
|
||||
config.retry_delay = Duration::from_millis(1);
|
||||
|
||||
let executor = ActionExecutor::new(gate, config);
|
||||
|
||||
let action = TrackedAction::failing("test.scope");
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
|
||||
let result = executor.execute(&action, &energy);
|
||||
|
||||
assert!(result.result.is_err());
|
||||
assert_eq!(action.execution_count(), 3); // Initial + 2 retries
|
||||
assert_eq!(result.stats.retry_count, 2);
|
||||
|
||||
let stats = executor.stats();
|
||||
assert_eq!(stats.total_failed, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_witness_storage() {
|
||||
let gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
// Execute multiple actions
|
||||
for i in 0..5 {
|
||||
let action = TrackedAction::new(&format!("test.scope.{}", i));
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
executor.execute(&action, &energy);
|
||||
}
|
||||
|
||||
let witnesses = executor.recent_witnesses(10);
|
||||
assert_eq!(witnesses.len(), 5);
|
||||
|
||||
// Witnesses should be in reverse chronological order
|
||||
for witness in &witnesses {
|
||||
assert!(witness.verify_integrity());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_stats() {
|
||||
let gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
// Mix of successful and denied
|
||||
for i in 0..10 {
|
||||
let action = TrackedAction::new(&format!("test.scope.{}", i));
|
||||
let energy = if i % 3 == 0 {
|
||||
EnergySnapshot::new(0.95, 0.9, action.scope.clone()) // Will be denied
|
||||
} else {
|
||||
EnergySnapshot::new(0.1, 0.05, action.scope.clone()) // Will succeed
|
||||
};
|
||||
executor.execute(&action, &energy);
|
||||
}
|
||||
|
||||
let stats = executor.stats();
|
||||
assert_eq!(stats.total_submitted, 10);
|
||||
assert!(stats.total_denied > 0);
|
||||
assert!(stats.total_success > 0);
|
||||
assert!(stats.success_rate() > 0.0);
|
||||
assert!(stats.denial_rate() > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_executor_clone() {
|
||||
let gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
let executor2 = executor.clone();
|
||||
|
||||
// Execute on original
|
||||
let action = TrackedAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
executor.execute(&action, &energy);
|
||||
|
||||
// Stats should be shared
|
||||
assert_eq!(
|
||||
executor.stats().total_submitted,
|
||||
executor2.stats().total_submitted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_result_builder() {
|
||||
let action_id = ActionId::new();
|
||||
|
||||
let success = ActionResultBuilder::new(action_id.clone(), ComputeLane::Reflex)
|
||||
.duration_us(500)
|
||||
.build();
|
||||
assert!(success.success);
|
||||
|
||||
let failure = ActionResultBuilder::new(action_id, ComputeLane::Retrieval)
|
||||
.failed("Test error")
|
||||
.duration_us(1000)
|
||||
.retries(2)
|
||||
.build();
|
||||
assert!(!failure.success);
|
||||
assert_eq!(failure.retry_count, 2);
|
||||
}
|
||||
}
|
||||
858
vendor/ruvector/crates/prime-radiant/src/execution/gate.rs
vendored
Normal file
858
vendor/ruvector/crates/prime-radiant/src/execution/gate.rs
vendored
Normal file
@@ -0,0 +1,858 @@
|
||||
//! # Coherence Gate: Threshold-Based Action Gating
|
||||
//!
|
||||
//! The coherence gate is the core decision point that controls whether actions
|
||||
//! are allowed to execute. It implements the ADR-014 gating logic:
|
||||
//!
|
||||
//! > Gate = refusal mechanism with witness
|
||||
//!
|
||||
//! ## Key Design Principles
|
||||
//!
|
||||
//! 1. **Most updates stay in reflex lane** - Low energy = automatic approval
|
||||
//! 2. **Persistence detection** - Energy above threshold for duration triggers escalation
|
||||
//! 3. **Mandatory witness creation** - Every decision produces an auditable record
|
||||
//! 4. **Policy bundle reference** - All decisions reference signed governance
|
||||
//!
|
||||
//! ## Gating Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! Action Request
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────┐
|
||||
//! │ Compute Energy │ ← Scoped energy from coherence engine
|
||||
//! └─────────────────┘
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────┐
|
||||
//! │ Check Threshold │ ← Lane thresholds from policy bundle
|
||||
//! └─────────────────┘
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────┐
|
||||
//! │ Check Persistence│ ← Energy history for this scope
|
||||
//! └─────────────────┘
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────┐
|
||||
//! │ Create Witness │ ← Mandatory for every decision
|
||||
//! └─────────────────┘
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────┐
|
||||
//! │ Return Decision │ → Allow, Escalate, or Deny
|
||||
//! └─────────────────┘
|
||||
//! ```
|
||||
|
||||
use super::action::{Action, ActionId, ActionImpact, ScopeId};
|
||||
use super::ladder::{ComputeLane, EscalationReason, LaneThresholds, LaneTransition};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Unique identifier for a policy bundle.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct PolicyBundleRef {
|
||||
/// Bundle ID.
|
||||
pub id: uuid::Uuid,
|
||||
/// Bundle version.
|
||||
pub version: String,
|
||||
/// Content hash for integrity verification.
|
||||
pub content_hash: [u8; 32],
|
||||
}
|
||||
|
||||
impl PolicyBundleRef {
|
||||
/// Create a new policy bundle reference.
|
||||
pub fn new(id: uuid::Uuid, version: impl Into<String>, content_hash: [u8; 32]) -> Self {
|
||||
Self {
|
||||
id,
|
||||
version: version.into(),
|
||||
content_hash,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a placeholder reference for testing.
|
||||
pub fn placeholder() -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::nil(),
|
||||
version: "0.0.0-test".to_string(),
|
||||
content_hash: [0u8; 32],
|
||||
}
|
||||
}
|
||||
|
||||
/// Get bytes representation for hashing.
|
||||
pub fn as_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(16 + self.version.len() + 32);
|
||||
bytes.extend_from_slice(self.id.as_bytes());
|
||||
bytes.extend_from_slice(self.version.as_bytes());
|
||||
bytes.extend_from_slice(&self.content_hash);
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// Unique identifier for a witness record.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct WitnessId(pub uuid::Uuid);
|
||||
|
||||
impl WitnessId {
|
||||
/// Generate a new random witness ID.
|
||||
pub fn new() -> Self {
|
||||
Self(uuid::Uuid::new_v4())
|
||||
}
|
||||
|
||||
/// Create from an existing UUID.
|
||||
pub fn from_uuid(uuid: uuid::Uuid) -> Self {
|
||||
Self(uuid)
|
||||
}
|
||||
}
|
||||
|
||||
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, "witness-{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot of coherence energy at the time of a gate decision.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnergySnapshot {
|
||||
/// Total system energy.
|
||||
pub total_energy: f32,
|
||||
/// Energy for the action's scope.
|
||||
pub scope_energy: f32,
|
||||
/// Scope that was evaluated.
|
||||
pub scope: ScopeId,
|
||||
/// Timestamp of snapshot (Unix millis).
|
||||
pub timestamp_ms: u64,
|
||||
/// Fingerprint for change detection.
|
||||
pub fingerprint: [u8; 32],
|
||||
}
|
||||
|
||||
impl EnergySnapshot {
|
||||
/// Create a new energy snapshot.
|
||||
pub fn new(total_energy: f32, scope_energy: f32, scope: ScopeId) -> Self {
|
||||
let timestamp_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut fingerprint = [0u8; 32];
|
||||
let hash_input = format!(
|
||||
"{}:{}:{}:{}",
|
||||
total_energy,
|
||||
scope_energy,
|
||||
scope.as_str(),
|
||||
timestamp_ms
|
||||
);
|
||||
let hash = blake3::hash(hash_input.as_bytes());
|
||||
fingerprint.copy_from_slice(hash.as_bytes());
|
||||
|
||||
Self {
|
||||
total_energy,
|
||||
scope_energy,
|
||||
scope,
|
||||
timestamp_ms,
|
||||
fingerprint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The gate's decision on an action.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GateDecision {
|
||||
/// Whether to allow the action.
|
||||
pub allow: bool,
|
||||
|
||||
/// Required compute lane for execution.
|
||||
pub lane: ComputeLane,
|
||||
|
||||
/// Reason if denied or escalated.
|
||||
pub reason: Option<String>,
|
||||
|
||||
/// Escalation details if applicable.
|
||||
pub escalation: Option<EscalationReason>,
|
||||
}
|
||||
|
||||
impl GateDecision {
|
||||
/// Create an allowing decision.
|
||||
pub fn allow(lane: ComputeLane) -> Self {
|
||||
Self {
|
||||
allow: true,
|
||||
lane,
|
||||
reason: None,
|
||||
escalation: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a denying decision.
|
||||
pub fn deny(reason: impl Into<String>) -> Self {
|
||||
Self {
|
||||
allow: false,
|
||||
lane: ComputeLane::Human, // Requires human intervention
|
||||
reason: Some(reason.into()),
|
||||
escalation: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an escalation decision.
|
||||
pub fn escalate(lane: ComputeLane, escalation: EscalationReason) -> Self {
|
||||
Self {
|
||||
allow: lane < ComputeLane::Human,
|
||||
lane,
|
||||
reason: Some(format!("Escalated: {}", escalation)),
|
||||
escalation: Some(escalation),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this decision requires escalation.
|
||||
pub fn is_escalated(&self) -> bool {
|
||||
self.escalation.is_some()
|
||||
}
|
||||
|
||||
/// Whether this decision allows automatic execution.
|
||||
pub fn allows_automatic_execution(&self) -> bool {
|
||||
self.allow && self.lane.allows_automatic_execution()
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable witness record for every gate decision.
|
||||
///
|
||||
/// This is the audit trail for the coherence engine. Every decision
|
||||
/// produces a witness that can be verified and replayed.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WitnessRecord {
|
||||
/// Unique witness identifier.
|
||||
pub id: WitnessId,
|
||||
|
||||
/// Hash of the action that was evaluated.
|
||||
pub action_hash: [u8; 32],
|
||||
|
||||
/// Action ID reference.
|
||||
pub action_id: ActionId,
|
||||
|
||||
/// Energy snapshot at evaluation time.
|
||||
pub energy_snapshot: EnergySnapshot,
|
||||
|
||||
/// The gate decision made.
|
||||
pub decision: GateDecision,
|
||||
|
||||
/// Policy bundle used for decision.
|
||||
pub policy_bundle_ref: PolicyBundleRef,
|
||||
|
||||
/// Timestamp of decision (Unix millis).
|
||||
pub timestamp_ms: u64,
|
||||
|
||||
/// Hash chain reference to previous witness.
|
||||
pub previous_witness: Option<WitnessId>,
|
||||
|
||||
/// Content hash of this witness (for chain integrity).
|
||||
pub content_hash: [u8; 32],
|
||||
}
|
||||
|
||||
impl WitnessRecord {
|
||||
/// Create a new witness record.
|
||||
pub fn new(
|
||||
action_hash: [u8; 32],
|
||||
action_id: ActionId,
|
||||
energy_snapshot: EnergySnapshot,
|
||||
decision: GateDecision,
|
||||
policy_bundle_ref: PolicyBundleRef,
|
||||
previous_witness: Option<WitnessId>,
|
||||
) -> Self {
|
||||
let id = WitnessId::new();
|
||||
let timestamp_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut record = Self {
|
||||
id,
|
||||
action_hash,
|
||||
action_id,
|
||||
energy_snapshot,
|
||||
decision,
|
||||
policy_bundle_ref,
|
||||
timestamp_ms,
|
||||
previous_witness,
|
||||
content_hash: [0u8; 32],
|
||||
};
|
||||
|
||||
record.content_hash = record.compute_content_hash();
|
||||
record
|
||||
}
|
||||
|
||||
/// Compute the content hash for this witness.
|
||||
fn compute_content_hash(&self) -> [u8; 32] {
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(&self.action_hash);
|
||||
hasher.update(self.action_id.as_bytes());
|
||||
hasher.update(&self.energy_snapshot.fingerprint);
|
||||
hasher.update(&(self.decision.allow as u8).to_le_bytes());
|
||||
hasher.update(&(self.decision.lane.as_u8()).to_le_bytes());
|
||||
hasher.update(&self.policy_bundle_ref.as_bytes());
|
||||
hasher.update(&self.timestamp_ms.to_le_bytes());
|
||||
|
||||
if let Some(ref prev) = self.previous_witness {
|
||||
hasher.update(prev.0.as_bytes());
|
||||
}
|
||||
|
||||
let mut hash = [0u8; 32];
|
||||
hash.copy_from_slice(hasher.finalize().as_bytes());
|
||||
hash
|
||||
}
|
||||
|
||||
/// Verify the content hash integrity.
|
||||
pub fn verify_integrity(&self) -> bool {
|
||||
self.content_hash == self.compute_content_hash()
|
||||
}
|
||||
}
|
||||
|
||||
/// Energy history tracker for persistence detection.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EnergyHistory {
|
||||
/// Per-scope energy histories (timestamp_ms, energy).
|
||||
histories: HashMap<ScopeId, Vec<(u64, f32)>>,
|
||||
|
||||
/// Maximum history entries per scope.
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl EnergyHistory {
|
||||
/// Create a new energy history tracker.
|
||||
pub fn new(max_entries: usize) -> Self {
|
||||
Self {
|
||||
histories: HashMap::new(),
|
||||
max_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record an energy observation for a scope.
|
||||
pub fn record(&mut self, scope: &ScopeId, energy: f32) {
|
||||
let timestamp_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let history = self.histories.entry(scope.clone()).or_default();
|
||||
history.push((timestamp_ms, energy));
|
||||
|
||||
// Trim old entries
|
||||
if history.len() > self.max_entries {
|
||||
history.drain(0..(history.len() - self.max_entries));
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if energy has been above threshold for the given duration.
|
||||
pub fn is_above_threshold(&self, scope: &ScopeId, threshold: f32, duration: Duration) -> bool {
|
||||
let history = match self.histories.get(scope) {
|
||||
Some(h) => h,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if history.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let duration_ms = duration.as_millis() as u64;
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
let window_start = now_ms.saturating_sub(duration_ms);
|
||||
|
||||
// Check if all readings in the window are above threshold
|
||||
let readings_in_window: Vec<_> = history
|
||||
.iter()
|
||||
.filter(|(ts, _)| *ts >= window_start)
|
||||
.collect();
|
||||
|
||||
if readings_in_window.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Need at least 2 readings and all must be above threshold
|
||||
readings_in_window.len() >= 2 && readings_in_window.iter().all(|(_, e)| *e >= threshold)
|
||||
}
|
||||
|
||||
/// Get the duration that energy has been above threshold.
|
||||
pub fn duration_above_threshold(&self, scope: &ScopeId, threshold: f32) -> Option<Duration> {
|
||||
let history = self.histories.get(scope)?;
|
||||
|
||||
if history.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the first reading above threshold, counting backwards
|
||||
let mut start_ts = None;
|
||||
for (ts, energy) in history.iter().rev() {
|
||||
if *energy >= threshold {
|
||||
start_ts = Some(*ts);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
start_ts.map(|start| {
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
Duration::from_millis(now_ms.saturating_sub(start))
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear history for a scope.
|
||||
pub fn clear_scope(&mut self, scope: &ScopeId) {
|
||||
self.histories.remove(scope);
|
||||
}
|
||||
|
||||
/// Clear all history.
|
||||
pub fn clear_all(&mut self) {
|
||||
self.histories.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// The coherence gate with configurable thresholds.
|
||||
///
|
||||
/// This is the main gating mechanism that controls action execution
|
||||
/// based on coherence energy levels and persistence detection.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CoherenceGate {
|
||||
/// Lane thresholds for energy-based escalation.
|
||||
thresholds: LaneThresholds,
|
||||
|
||||
/// Persistence window for detecting sustained incoherence.
|
||||
persistence_window: Duration,
|
||||
|
||||
/// Reference to the active policy bundle.
|
||||
policy_bundle: PolicyBundleRef,
|
||||
|
||||
/// Energy history for persistence detection.
|
||||
history: EnergyHistory,
|
||||
|
||||
/// Last witness ID for chaining.
|
||||
last_witness_id: Option<WitnessId>,
|
||||
|
||||
/// Lane transition history.
|
||||
transitions: Vec<LaneTransition>,
|
||||
|
||||
/// Maximum transitions to keep.
|
||||
max_transitions: usize,
|
||||
}
|
||||
|
||||
impl CoherenceGate {
|
||||
/// Create a new coherence gate with the given configuration.
|
||||
pub fn new(
|
||||
thresholds: LaneThresholds,
|
||||
persistence_window: Duration,
|
||||
policy_bundle: PolicyBundleRef,
|
||||
) -> Self {
|
||||
Self {
|
||||
thresholds,
|
||||
persistence_window,
|
||||
policy_bundle,
|
||||
history: EnergyHistory::new(1000),
|
||||
last_witness_id: None,
|
||||
transitions: Vec::new(),
|
||||
max_transitions: 100,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a gate with default configuration.
|
||||
pub fn with_defaults(policy_bundle: PolicyBundleRef) -> Self {
|
||||
Self::new(
|
||||
LaneThresholds::default(),
|
||||
Duration::from_secs(5),
|
||||
policy_bundle,
|
||||
)
|
||||
}
|
||||
|
||||
/// Evaluate whether an action should proceed.
|
||||
///
|
||||
/// This is the core gating method that:
|
||||
/// 1. Determines required lane based on energy
|
||||
/// 2. Checks for persistent incoherence
|
||||
/// 3. Creates mandatory witness record
|
||||
/// 4. Returns the gate decision
|
||||
#[inline]
|
||||
pub fn evaluate<A: Action>(&mut self, action: &A, energy: &EnergySnapshot) -> GateDecision {
|
||||
let current_energy = energy.scope_energy;
|
||||
|
||||
// FAST PATH: Low energy and low-risk action -> immediate reflex approval
|
||||
// This bypasses most computation for the common case (ADR-014 reflex lane)
|
||||
if current_energy < self.thresholds.reflex {
|
||||
let impact = action.impact();
|
||||
if !impact.is_high_risk() {
|
||||
// Quick history record and return
|
||||
self.history.record(action.scope(), current_energy);
|
||||
return GateDecision::allow(ComputeLane::Reflex);
|
||||
}
|
||||
}
|
||||
|
||||
// STANDARD PATH: Full evaluation for higher energy or high-risk actions
|
||||
self.evaluate_full(action, energy)
|
||||
}
|
||||
|
||||
/// Full evaluation path for non-trivial cases
|
||||
#[inline(never)] // Keep this out-of-line to keep fast path small
|
||||
fn evaluate_full<A: Action>(&mut self, action: &A, energy: &EnergySnapshot) -> GateDecision {
|
||||
let scope = action.scope();
|
||||
let impact = action.impact();
|
||||
let current_energy = energy.scope_energy;
|
||||
|
||||
// Record energy observation
|
||||
self.history.record(scope, current_energy);
|
||||
|
||||
// Determine base lane from energy using branchless comparison
|
||||
let mut lane = self.thresholds.lane_for_energy(current_energy);
|
||||
|
||||
// Adjust for action impact
|
||||
if impact.is_high_risk() && lane < ComputeLane::Retrieval {
|
||||
lane = ComputeLane::Retrieval;
|
||||
}
|
||||
|
||||
// Check for persistent incoherence
|
||||
let persistent =
|
||||
self.history
|
||||
.is_above_threshold(scope, self.thresholds.reflex, self.persistence_window);
|
||||
|
||||
let escalation = if persistent && lane < ComputeLane::Heavy {
|
||||
// Persistent incoherence requires at least Heavy lane
|
||||
let duration = self
|
||||
.history
|
||||
.duration_above_threshold(scope, self.thresholds.reflex)
|
||||
.unwrap_or_default();
|
||||
|
||||
let reason = EscalationReason::persistent(
|
||||
duration.as_millis() as u64,
|
||||
self.persistence_window.as_millis() as u64,
|
||||
);
|
||||
|
||||
let old_lane = lane;
|
||||
lane = ComputeLane::Heavy;
|
||||
|
||||
// Record transition
|
||||
self.record_transition(old_lane, lane, reason.clone(), current_energy);
|
||||
|
||||
Some(reason)
|
||||
} else if current_energy >= self.thresholds.reflex {
|
||||
// Energy-based escalation
|
||||
let reason = EscalationReason::energy(current_energy, self.thresholds.reflex);
|
||||
|
||||
if lane > ComputeLane::Reflex {
|
||||
Some(reason)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build decision
|
||||
if lane == ComputeLane::Human {
|
||||
GateDecision::deny("Energy exceeds all automatic thresholds - requires human review")
|
||||
} else if let Some(escalation) = escalation {
|
||||
GateDecision::escalate(lane, escalation)
|
||||
} else {
|
||||
GateDecision::allow(lane)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast path evaluation that skips witness creation
|
||||
/// Use when witness is not needed (e.g., preflight checks)
|
||||
#[inline]
|
||||
pub fn evaluate_fast(&self, scope_energy: f32) -> ComputeLane {
|
||||
self.thresholds.lane_for_energy(scope_energy)
|
||||
}
|
||||
|
||||
/// Create a witness record for a gate decision.
|
||||
///
|
||||
/// This MUST be called for every evaluation to maintain the audit trail.
|
||||
pub fn create_witness<A: Action>(
|
||||
&mut self,
|
||||
action: &A,
|
||||
energy: &EnergySnapshot,
|
||||
decision: &GateDecision,
|
||||
) -> WitnessRecord {
|
||||
let witness = WitnessRecord::new(
|
||||
action.content_hash(),
|
||||
action.metadata().id.clone(),
|
||||
energy.clone(),
|
||||
decision.clone(),
|
||||
self.policy_bundle.clone(),
|
||||
self.last_witness_id.clone(),
|
||||
);
|
||||
|
||||
self.last_witness_id = Some(witness.id.clone());
|
||||
witness
|
||||
}
|
||||
|
||||
/// Evaluate and create witness in one call.
|
||||
pub fn evaluate_with_witness<A: Action>(
|
||||
&mut self,
|
||||
action: &A,
|
||||
energy: &EnergySnapshot,
|
||||
) -> (GateDecision, WitnessRecord) {
|
||||
let decision = self.evaluate(action, energy);
|
||||
let witness = self.create_witness(action, energy, &decision);
|
||||
(decision, witness)
|
||||
}
|
||||
|
||||
/// Record a lane transition.
|
||||
fn record_transition(
|
||||
&mut self,
|
||||
from: ComputeLane,
|
||||
to: ComputeLane,
|
||||
reason: EscalationReason,
|
||||
energy: f32,
|
||||
) {
|
||||
let transition = LaneTransition::new(from, to, reason, energy);
|
||||
self.transitions.push(transition);
|
||||
|
||||
// Trim old transitions
|
||||
if self.transitions.len() > self.max_transitions {
|
||||
self.transitions
|
||||
.drain(0..(self.transitions.len() - self.max_transitions));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get recent lane transitions.
|
||||
pub fn recent_transitions(&self) -> &[LaneTransition] {
|
||||
&self.transitions
|
||||
}
|
||||
|
||||
/// Update the policy bundle reference.
|
||||
pub fn update_policy_bundle(&mut self, bundle: PolicyBundleRef) {
|
||||
self.policy_bundle = bundle;
|
||||
}
|
||||
|
||||
/// Update the lane thresholds.
|
||||
pub fn update_thresholds(&mut self, thresholds: LaneThresholds) {
|
||||
self.thresholds = thresholds;
|
||||
}
|
||||
|
||||
/// Update the persistence window.
|
||||
pub fn update_persistence_window(&mut self, window: Duration) {
|
||||
self.persistence_window = window;
|
||||
}
|
||||
|
||||
/// Get current thresholds.
|
||||
pub fn thresholds(&self) -> &LaneThresholds {
|
||||
&self.thresholds
|
||||
}
|
||||
|
||||
/// Get current persistence window.
|
||||
pub fn persistence_window(&self) -> Duration {
|
||||
self.persistence_window
|
||||
}
|
||||
|
||||
/// Get current policy bundle reference.
|
||||
pub fn policy_bundle(&self) -> &PolicyBundleRef {
|
||||
&self.policy_bundle
|
||||
}
|
||||
|
||||
/// Clear energy history for a scope.
|
||||
pub fn clear_scope_history(&mut self, scope: &ScopeId) {
|
||||
self.history.clear_scope(scope);
|
||||
}
|
||||
|
||||
/// Reset the gate to initial state.
|
||||
pub fn reset(&mut self) {
|
||||
self.history.clear_all();
|
||||
self.last_witness_id = None;
|
||||
self.transitions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::execution::action::{ActionError, ActionMetadata, ExecutionContext};
|
||||
|
||||
// Test action implementation
|
||||
struct TestAction {
|
||||
scope: ScopeId,
|
||||
impact: ActionImpact,
|
||||
metadata: ActionMetadata,
|
||||
}
|
||||
|
||||
impl TestAction {
|
||||
fn new(scope: &str) -> Self {
|
||||
Self {
|
||||
scope: ScopeId::new(scope),
|
||||
impact: ActionImpact::low(),
|
||||
metadata: ActionMetadata::new("TestAction", "Test action", "test-actor"),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_impact(mut self, impact: ActionImpact) -> Self {
|
||||
self.impact = impact;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for TestAction {
|
||||
type Output = ();
|
||||
type Error = ActionError;
|
||||
|
||||
fn scope(&self) -> &ScopeId {
|
||||
&self.scope
|
||||
}
|
||||
|
||||
fn impact(&self) -> ActionImpact {
|
||||
self.impact
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &ActionMetadata {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
fn execute(&self, _ctx: &ExecutionContext) -> Result<(), ActionError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn content_hash(&self) -> [u8; 32] {
|
||||
let hash = blake3::hash(self.scope.as_str().as_bytes());
|
||||
let mut result = [0u8; 32];
|
||||
result.copy_from_slice(hash.as_bytes());
|
||||
result
|
||||
}
|
||||
|
||||
fn make_rollback_not_supported_error() -> ActionError {
|
||||
ActionError::RollbackNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_low_energy_allows_reflex() {
|
||||
let mut gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let action = TestAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
|
||||
let decision = gate.evaluate(&action, &energy);
|
||||
|
||||
assert!(decision.allow);
|
||||
assert_eq!(decision.lane, ComputeLane::Reflex);
|
||||
assert!(!decision.is_escalated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_medium_energy_escalates() {
|
||||
let mut gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let action = TestAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.4, 0.35, action.scope.clone());
|
||||
|
||||
let decision = gate.evaluate(&action, &energy);
|
||||
|
||||
assert!(decision.allow);
|
||||
assert_eq!(decision.lane, ComputeLane::Retrieval);
|
||||
assert!(decision.is_escalated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_high_energy_heavy_lane() {
|
||||
let mut gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let action = TestAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.7, 0.65, action.scope.clone());
|
||||
|
||||
let decision = gate.evaluate(&action, &energy);
|
||||
|
||||
assert!(decision.allow);
|
||||
assert_eq!(decision.lane, ComputeLane::Heavy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_extreme_energy_denies() {
|
||||
let mut gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let action = TestAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.95, 0.9, action.scope.clone());
|
||||
|
||||
let decision = gate.evaluate(&action, &energy);
|
||||
|
||||
assert!(!decision.allow);
|
||||
assert_eq!(decision.lane, ComputeLane::Human);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_high_risk_impact_escalates() {
|
||||
let mut gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let action = TestAction::new("test.scope").with_impact(ActionImpact::critical());
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
|
||||
let decision = gate.evaluate(&action, &energy);
|
||||
|
||||
// Even low energy gets escalated due to high-risk action
|
||||
assert!(decision.allow);
|
||||
assert!(decision.lane >= ComputeLane::Retrieval);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_witness_record_integrity() {
|
||||
let mut gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let action = TestAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
|
||||
let (decision, witness) = gate.evaluate_with_witness(&action, &energy);
|
||||
|
||||
assert!(witness.verify_integrity());
|
||||
assert_eq!(witness.decision.allow, decision.allow);
|
||||
assert_eq!(witness.decision.lane, decision.lane);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_witness_chain() {
|
||||
let mut gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let action = TestAction::new("test.scope");
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
|
||||
// First witness
|
||||
let (_, witness1) = gate.evaluate_with_witness(&action, &energy);
|
||||
assert!(witness1.previous_witness.is_none());
|
||||
|
||||
// Second witness should chain to first
|
||||
let (_, witness2) = gate.evaluate_with_witness(&action, &energy);
|
||||
assert_eq!(witness2.previous_witness, Some(witness1.id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_energy_history() {
|
||||
let mut history = EnergyHistory::new(100);
|
||||
let scope = ScopeId::new("test");
|
||||
|
||||
// Record some energy values
|
||||
for _ in 0..5 {
|
||||
history.record(&scope, 0.5);
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
|
||||
// Should detect persistent high energy
|
||||
assert!(history.is_above_threshold(&scope, 0.3, Duration::from_millis(30)));
|
||||
|
||||
// Should not detect if threshold too high
|
||||
assert!(!history.is_above_threshold(&scope, 0.6, Duration::from_millis(30)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gate_transitions_recorded() {
|
||||
let mut gate = CoherenceGate::with_defaults(PolicyBundleRef::placeholder());
|
||||
let action = TestAction::new("test.scope");
|
||||
|
||||
// Record multiple evaluations that should trigger persistence
|
||||
for _ in 0..10 {
|
||||
let energy = EnergySnapshot::new(0.4, 0.35, action.scope.clone());
|
||||
gate.evaluate(&action, &energy);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// After multiple high-energy evaluations, may have recorded transitions
|
||||
// Note: exact behavior depends on timing
|
||||
let transitions = gate.recent_transitions();
|
||||
// Just verify we can access transitions without panic
|
||||
assert!(transitions.len() <= gate.max_transitions);
|
||||
}
|
||||
}
|
||||
577
vendor/ruvector/crates/prime-radiant/src/execution/ladder.rs
vendored
Normal file
577
vendor/ruvector/crates/prime-radiant/src/execution/ladder.rs
vendored
Normal file
@@ -0,0 +1,577 @@
|
||||
//! # Compute Ladder: Escalation Logic for Coherence-Gated Execution
|
||||
//!
|
||||
//! Implements the compute ladder from ADR-014, providing threshold-based escalation
|
||||
//! from low-latency reflex operations to human-in-the-loop review.
|
||||
//!
|
||||
//! ## Design Principle
|
||||
//!
|
||||
//! > Most updates stay in low-latency reflex lane (<1ms); sustained/growing
|
||||
//! > incoherence triggers escalation.
|
||||
//!
|
||||
//! The compute ladder is not about being smart - it's about knowing when to stop
|
||||
//! and when to ask for help.
|
||||
//!
|
||||
//! ## Lanes
|
||||
//!
|
||||
//! | Lane | Name | Latency | Description |
|
||||
//! |------|------|---------|-------------|
|
||||
//! | 0 | Reflex | <1ms | Local residual updates, simple aggregates |
|
||||
//! | 1 | Retrieval | ~10ms | Evidence fetching, lightweight reasoning |
|
||||
//! | 2 | Heavy | ~100ms | Multi-step planning, spectral analysis |
|
||||
//! | 3 | Human | async | Human escalation for sustained incoherence |
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
/// Compute lanes for escalating complexity.
|
||||
///
|
||||
/// CRITICAL: Most updates stay in Lane 0 (Reflex).
|
||||
/// Escalation only occurs on sustained/growing incoherence.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum ComputeLane {
|
||||
/// Lane 0: Local residual updates, simple aggregates (<1ms)
|
||||
/// THE DEFAULT - most updates stay here
|
||||
Reflex = 0,
|
||||
|
||||
/// Lane 1: Evidence fetching, lightweight reasoning (~10ms)
|
||||
/// Triggered by: transient energy spike
|
||||
Retrieval = 1,
|
||||
|
||||
/// Lane 2: Multi-step planning, spectral analysis (~100ms)
|
||||
/// Triggered by: sustained incoherence above threshold
|
||||
Heavy = 2,
|
||||
|
||||
/// Lane 3: Human escalation for sustained incoherence
|
||||
/// Triggered by: persistent incoherence that automated systems cannot resolve
|
||||
Human = 3,
|
||||
}
|
||||
|
||||
impl ComputeLane {
|
||||
/// Get the expected latency budget for this lane in microseconds.
|
||||
#[inline]
|
||||
pub const fn latency_budget_us(&self) -> u64 {
|
||||
match self {
|
||||
ComputeLane::Reflex => 1_000, // 1ms
|
||||
ComputeLane::Retrieval => 10_000, // 10ms
|
||||
ComputeLane::Heavy => 100_000, // 100ms
|
||||
ComputeLane::Human => u64::MAX, // No limit (async)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the expected latency budget for this lane in milliseconds.
|
||||
#[inline]
|
||||
pub const fn latency_budget_ms(&self) -> u64 {
|
||||
match self {
|
||||
ComputeLane::Reflex => 1,
|
||||
ComputeLane::Retrieval => 10,
|
||||
ComputeLane::Heavy => 100,
|
||||
ComputeLane::Human => u64::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this lane allows automatic action execution.
|
||||
///
|
||||
/// Returns `false` only for Human lane, which requires explicit approval.
|
||||
#[inline]
|
||||
pub const fn allows_automatic_execution(&self) -> bool {
|
||||
!matches!(self, ComputeLane::Human)
|
||||
}
|
||||
|
||||
/// Whether this lane is the default low-latency lane.
|
||||
#[inline]
|
||||
pub const fn is_reflex(&self) -> bool {
|
||||
matches!(self, ComputeLane::Reflex)
|
||||
}
|
||||
|
||||
/// Whether this lane requires escalation (not reflex).
|
||||
#[inline]
|
||||
pub const fn is_escalated(&self) -> bool {
|
||||
!matches!(self, ComputeLane::Reflex)
|
||||
}
|
||||
|
||||
/// Get the next escalation level, if any.
|
||||
pub const fn escalate(&self) -> Option<ComputeLane> {
|
||||
match self {
|
||||
ComputeLane::Reflex => Some(ComputeLane::Retrieval),
|
||||
ComputeLane::Retrieval => Some(ComputeLane::Heavy),
|
||||
ComputeLane::Heavy => Some(ComputeLane::Human),
|
||||
ComputeLane::Human => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the previous de-escalation level, if any.
|
||||
pub const fn deescalate(&self) -> Option<ComputeLane> {
|
||||
match self {
|
||||
ComputeLane::Reflex => None,
|
||||
ComputeLane::Retrieval => Some(ComputeLane::Reflex),
|
||||
ComputeLane::Heavy => Some(ComputeLane::Retrieval),
|
||||
ComputeLane::Human => Some(ComputeLane::Heavy),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse from u8 value.
|
||||
pub const fn from_u8(value: u8) -> Option<ComputeLane> {
|
||||
match value {
|
||||
0 => Some(ComputeLane::Reflex),
|
||||
1 => Some(ComputeLane::Retrieval),
|
||||
2 => Some(ComputeLane::Heavy),
|
||||
3 => Some(ComputeLane::Human),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to u8 value.
|
||||
#[inline]
|
||||
pub const fn as_u8(&self) -> u8 {
|
||||
*self as u8
|
||||
}
|
||||
|
||||
/// Get a human-readable name for this lane.
|
||||
pub const fn name(&self) -> &'static str {
|
||||
match self {
|
||||
ComputeLane::Reflex => "Reflex",
|
||||
ComputeLane::Retrieval => "Retrieval",
|
||||
ComputeLane::Heavy => "Heavy",
|
||||
ComputeLane::Human => "Human",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a description of what triggers this lane.
|
||||
pub const fn trigger_description(&self) -> &'static str {
|
||||
match self {
|
||||
ComputeLane::Reflex => "Default lane - low energy, no trigger needed",
|
||||
ComputeLane::Retrieval => "Transient energy spike above reflex threshold",
|
||||
ComputeLane::Heavy => "Sustained incoherence above retrieval threshold",
|
||||
ComputeLane::Human => "Persistent incoherence exceeding all automatic thresholds",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ComputeLane {
|
||||
fn default() -> Self {
|
||||
ComputeLane::Reflex
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ComputeLane {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Lane {} ({})", self.as_u8(), self.name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Threshold configuration for compute lane escalation.
|
||||
///
|
||||
/// These thresholds determine when energy levels trigger lane transitions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LaneThresholds {
|
||||
/// Energy threshold for Lane 0 (Reflex) - stay in reflex if below this
|
||||
pub reflex: f32,
|
||||
|
||||
/// Energy threshold for Lane 1 (Retrieval) - escalate to retrieval if above reflex
|
||||
pub retrieval: f32,
|
||||
|
||||
/// Energy threshold for Lane 2 (Heavy) - escalate to heavy if above retrieval
|
||||
pub heavy: f32,
|
||||
}
|
||||
|
||||
impl LaneThresholds {
|
||||
/// Create thresholds with explicit values.
|
||||
pub const fn new(reflex: f32, retrieval: f32, heavy: f32) -> Self {
|
||||
Self {
|
||||
reflex,
|
||||
retrieval,
|
||||
heavy,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create conservative thresholds (prefer escalation).
|
||||
pub const fn conservative() -> Self {
|
||||
Self {
|
||||
reflex: 0.1,
|
||||
retrieval: 0.3,
|
||||
heavy: 0.6,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create aggressive thresholds (prefer staying in reflex).
|
||||
pub const fn aggressive() -> Self {
|
||||
Self {
|
||||
reflex: 0.5,
|
||||
retrieval: 0.8,
|
||||
heavy: 0.95,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that thresholds are properly ordered.
|
||||
pub fn validate(&self) -> Result<(), ThresholdError> {
|
||||
if self.reflex < 0.0 || self.reflex > 1.0 {
|
||||
return Err(ThresholdError::OutOfRange {
|
||||
name: "reflex",
|
||||
value: self.reflex,
|
||||
});
|
||||
}
|
||||
if self.retrieval < 0.0 || self.retrieval > 1.0 {
|
||||
return Err(ThresholdError::OutOfRange {
|
||||
name: "retrieval",
|
||||
value: self.retrieval,
|
||||
});
|
||||
}
|
||||
if self.heavy < 0.0 || self.heavy > 1.0 {
|
||||
return Err(ThresholdError::OutOfRange {
|
||||
name: "heavy",
|
||||
value: self.heavy,
|
||||
});
|
||||
}
|
||||
if self.reflex >= self.retrieval {
|
||||
return Err(ThresholdError::InvalidOrdering {
|
||||
lower: "reflex",
|
||||
upper: "retrieval",
|
||||
});
|
||||
}
|
||||
if self.retrieval >= self.heavy {
|
||||
return Err(ThresholdError::InvalidOrdering {
|
||||
lower: "retrieval",
|
||||
upper: "heavy",
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determine which lane an energy level requires.
|
||||
///
|
||||
/// Optimized with branchless comparison using conditional moves
|
||||
/// for better branch prediction on modern CPUs.
|
||||
#[inline]
|
||||
pub fn lane_for_energy(&self, energy: f32) -> ComputeLane {
|
||||
// Use branchless comparison for better performance
|
||||
// The compiler can convert this to conditional moves (CMOVcc)
|
||||
let is_above_reflex = (energy >= self.reflex) as u8;
|
||||
let is_above_retrieval = (energy >= self.retrieval) as u8;
|
||||
let is_above_heavy = (energy >= self.heavy) as u8;
|
||||
|
||||
// Sum determines the lane: 0=Reflex, 1=Retrieval, 2=Heavy, 3=Human
|
||||
let lane_index = is_above_reflex + is_above_retrieval + is_above_heavy;
|
||||
|
||||
// SAFETY: lane_index is guaranteed to be 0-3
|
||||
match lane_index {
|
||||
0 => ComputeLane::Reflex,
|
||||
1 => ComputeLane::Retrieval,
|
||||
2 => ComputeLane::Heavy,
|
||||
_ => ComputeLane::Human,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast lane check using array lookup (alternative implementation)
|
||||
#[inline]
|
||||
pub fn lane_for_energy_lookup(&self, energy: f32) -> ComputeLane {
|
||||
// Store thresholds in array for potential SIMD comparison
|
||||
let thresholds = [self.reflex, self.retrieval, self.heavy];
|
||||
|
||||
// Count how many thresholds are exceeded
|
||||
let mut lane = 0u8;
|
||||
for &t in &thresholds {
|
||||
lane += (energy >= t) as u8;
|
||||
}
|
||||
|
||||
// SAFETY: lane is 0-3
|
||||
ComputeLane::from_u8(lane).unwrap_or(ComputeLane::Human)
|
||||
}
|
||||
|
||||
/// Get the threshold for a specific lane transition.
|
||||
pub fn threshold_for_lane(&self, lane: ComputeLane) -> f32 {
|
||||
match lane {
|
||||
ComputeLane::Reflex => 0.0, // Always accessible
|
||||
ComputeLane::Retrieval => self.reflex,
|
||||
ComputeLane::Heavy => self.retrieval,
|
||||
ComputeLane::Human => self.heavy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LaneThresholds {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
reflex: 0.2,
|
||||
retrieval: 0.5,
|
||||
heavy: 0.8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for threshold validation.
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum ThresholdError {
|
||||
#[error("Threshold '{name}' value {value} is out of range [0.0, 1.0]")]
|
||||
OutOfRange { name: &'static str, value: f32 },
|
||||
|
||||
#[error("Invalid threshold ordering: {lower} must be less than {upper}")]
|
||||
InvalidOrdering {
|
||||
lower: &'static str,
|
||||
upper: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
/// Escalation reason describing why a lane transition occurred.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum EscalationReason {
|
||||
/// Energy exceeded threshold for current lane.
|
||||
EnergyThreshold {
|
||||
/// The measured energy level.
|
||||
energy: u32, // Fixed point (energy * 1000)
|
||||
/// The threshold that was exceeded.
|
||||
threshold: u32,
|
||||
},
|
||||
|
||||
/// Persistent incoherence detected (energy above threshold for duration).
|
||||
PersistentIncoherence {
|
||||
/// Duration in milliseconds that energy was elevated.
|
||||
duration_ms: u64,
|
||||
/// Configured persistence window in milliseconds.
|
||||
window_ms: u64,
|
||||
},
|
||||
|
||||
/// Growing incoherence trend detected.
|
||||
GrowingIncoherence {
|
||||
/// Energy growth rate per second.
|
||||
growth_rate: i32, // Fixed point (rate * 1000)
|
||||
},
|
||||
|
||||
/// External trigger requested escalation.
|
||||
ExternalTrigger {
|
||||
/// Source of the trigger.
|
||||
source: String,
|
||||
},
|
||||
|
||||
/// System override (e.g., maintenance mode).
|
||||
SystemOverride {
|
||||
/// Reason for override.
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl EscalationReason {
|
||||
/// Create an energy threshold escalation.
|
||||
pub fn energy(energy: f32, threshold: f32) -> Self {
|
||||
Self::EnergyThreshold {
|
||||
energy: (energy * 1000.0) as u32,
|
||||
threshold: (threshold * 1000.0) as u32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a persistent incoherence escalation.
|
||||
pub fn persistent(duration_ms: u64, window_ms: u64) -> Self {
|
||||
Self::PersistentIncoherence {
|
||||
duration_ms,
|
||||
window_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a growing incoherence escalation.
|
||||
pub fn growing(growth_rate: f32) -> Self {
|
||||
Self::GrowingIncoherence {
|
||||
growth_rate: (growth_rate * 1000.0) as i32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Is this a persistence-based escalation?
|
||||
pub fn is_persistence_based(&self) -> bool {
|
||||
matches!(self, Self::PersistentIncoherence { .. })
|
||||
}
|
||||
|
||||
/// Is this an external trigger?
|
||||
pub fn is_external(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ExternalTrigger { .. } | Self::SystemOverride { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EscalationReason {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::EnergyThreshold { energy, threshold } => {
|
||||
write!(
|
||||
f,
|
||||
"Energy {:.3} exceeded threshold {:.3}",
|
||||
*energy as f32 / 1000.0,
|
||||
*threshold as f32 / 1000.0
|
||||
)
|
||||
}
|
||||
Self::PersistentIncoherence {
|
||||
duration_ms,
|
||||
window_ms,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Persistent incoherence for {}ms (window: {}ms)",
|
||||
duration_ms, window_ms
|
||||
)
|
||||
}
|
||||
Self::GrowingIncoherence { growth_rate } => {
|
||||
write!(
|
||||
f,
|
||||
"Growing incoherence at {:.3}/s",
|
||||
*growth_rate as f32 / 1000.0
|
||||
)
|
||||
}
|
||||
Self::ExternalTrigger { source } => {
|
||||
write!(f, "External trigger from: {}", source)
|
||||
}
|
||||
Self::SystemOverride { reason } => {
|
||||
write!(f, "System override: {}", reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Lane transition record for audit trail.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LaneTransition {
|
||||
/// Previous lane.
|
||||
pub from_lane: ComputeLane,
|
||||
|
||||
/// New lane.
|
||||
pub to_lane: ComputeLane,
|
||||
|
||||
/// Reason for transition.
|
||||
pub reason: EscalationReason,
|
||||
|
||||
/// Timestamp of transition (Unix millis).
|
||||
pub timestamp_ms: u64,
|
||||
|
||||
/// Energy at time of transition.
|
||||
pub energy: f32,
|
||||
}
|
||||
|
||||
impl LaneTransition {
|
||||
/// Create a new lane transition record.
|
||||
pub fn new(
|
||||
from_lane: ComputeLane,
|
||||
to_lane: ComputeLane,
|
||||
reason: EscalationReason,
|
||||
energy: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
from_lane,
|
||||
to_lane,
|
||||
reason,
|
||||
timestamp_ms: Self::current_timestamp_ms(),
|
||||
energy,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current timestamp in milliseconds.
|
||||
fn current_timestamp_ms() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Whether this is an escalation (moving to higher lane).
|
||||
pub fn is_escalation(&self) -> bool {
|
||||
self.to_lane > self.from_lane
|
||||
}
|
||||
|
||||
/// Whether this is a de-escalation (moving to lower lane).
|
||||
pub fn is_deescalation(&self) -> bool {
|
||||
self.to_lane < self.from_lane
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lane_ordering() {
|
||||
assert!(ComputeLane::Reflex < ComputeLane::Retrieval);
|
||||
assert!(ComputeLane::Retrieval < ComputeLane::Heavy);
|
||||
assert!(ComputeLane::Heavy < ComputeLane::Human);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lane_escalation() {
|
||||
assert_eq!(ComputeLane::Reflex.escalate(), Some(ComputeLane::Retrieval));
|
||||
assert_eq!(ComputeLane::Retrieval.escalate(), Some(ComputeLane::Heavy));
|
||||
assert_eq!(ComputeLane::Heavy.escalate(), Some(ComputeLane::Human));
|
||||
assert_eq!(ComputeLane::Human.escalate(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lane_deescalation() {
|
||||
assert_eq!(ComputeLane::Reflex.deescalate(), None);
|
||||
assert_eq!(
|
||||
ComputeLane::Retrieval.deescalate(),
|
||||
Some(ComputeLane::Reflex)
|
||||
);
|
||||
assert_eq!(
|
||||
ComputeLane::Heavy.deescalate(),
|
||||
Some(ComputeLane::Retrieval)
|
||||
);
|
||||
assert_eq!(ComputeLane::Human.deescalate(), Some(ComputeLane::Heavy));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lane_automatic_execution() {
|
||||
assert!(ComputeLane::Reflex.allows_automatic_execution());
|
||||
assert!(ComputeLane::Retrieval.allows_automatic_execution());
|
||||
assert!(ComputeLane::Heavy.allows_automatic_execution());
|
||||
assert!(!ComputeLane::Human.allows_automatic_execution());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_thresholds() {
|
||||
let thresholds = LaneThresholds::default();
|
||||
assert!(thresholds.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_threshold_validation() {
|
||||
// Valid thresholds
|
||||
let valid = LaneThresholds::new(0.1, 0.5, 0.9);
|
||||
assert!(valid.validate().is_ok());
|
||||
|
||||
// Invalid ordering
|
||||
let invalid = LaneThresholds::new(0.5, 0.3, 0.9);
|
||||
assert!(invalid.validate().is_err());
|
||||
|
||||
// Out of range
|
||||
let out_of_range = LaneThresholds::new(-0.1, 0.5, 0.9);
|
||||
assert!(out_of_range.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lane_for_energy() {
|
||||
let thresholds = LaneThresholds::new(0.2, 0.5, 0.8);
|
||||
|
||||
assert_eq!(thresholds.lane_for_energy(0.1), ComputeLane::Reflex);
|
||||
assert_eq!(thresholds.lane_for_energy(0.3), ComputeLane::Retrieval);
|
||||
assert_eq!(thresholds.lane_for_energy(0.6), ComputeLane::Heavy);
|
||||
assert_eq!(thresholds.lane_for_energy(0.9), ComputeLane::Human);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escalation_reason_display() {
|
||||
let reason = EscalationReason::energy(0.75, 0.5);
|
||||
assert!(reason.to_string().contains("exceeded threshold"));
|
||||
|
||||
let persistent = EscalationReason::persistent(5000, 3000);
|
||||
assert!(persistent.to_string().contains("5000ms"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lane_transition() {
|
||||
let transition = LaneTransition::new(
|
||||
ComputeLane::Reflex,
|
||||
ComputeLane::Retrieval,
|
||||
EscalationReason::energy(0.3, 0.2),
|
||||
0.3,
|
||||
);
|
||||
|
||||
assert!(transition.is_escalation());
|
||||
assert!(!transition.is_deescalation());
|
||||
}
|
||||
}
|
||||
298
vendor/ruvector/crates/prime-radiant/src/execution/mod.rs
vendored
Normal file
298
vendor/ruvector/crates/prime-radiant/src/execution/mod.rs
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
//! # Execution Module: Coherence-Gated Action Execution
|
||||
//!
|
||||
//! This module implements the coherence gate and compute ladder from ADR-014,
|
||||
//! providing threshold-based gating for external side effects with mandatory
|
||||
//! witness creation.
|
||||
//!
|
||||
//! ## Architecture Overview
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
//! │ ACTION EXECUTOR │
|
||||
//! │ Orchestrates the entire execution flow with mandatory witnesses │
|
||||
//! └─────────────────────────────────────────────────────────────────────────┘
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
//! │ COHERENCE GATE │
|
||||
//! │ Threshold-based gating with persistence detection │
|
||||
//! │ Policy bundle reference • Energy history • Witness creation │
|
||||
//! └─────────────────────────────────────────────────────────────────────────┘
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
//! │ COMPUTE LADDER │
|
||||
//! │ Lane 0 (Reflex) → Lane 1 (Retrieval) → Lane 2 (Heavy) → Lane 3 (Human)│
|
||||
//! │ <1ms ~10ms ~100ms async │
|
||||
//! └─────────────────────────────────────────────────────────────────────────┘
|
||||
//! │
|
||||
//! ▼
|
||||
//! ┌─────────────────────────────────────────────────────────────────────────┐
|
||||
//! │ ACTION TRAIT │
|
||||
//! │ Scope • Impact • Metadata • Execute • Content Hash │
|
||||
//! └─────────────────────────────────────────────────────────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Key Design Principles
|
||||
//!
|
||||
//! 1. **Most updates stay in reflex lane** - Low energy (<threshold) = fast path
|
||||
//! 2. **Persistence detection** - Sustained incoherence triggers escalation
|
||||
//! 3. **Mandatory witness creation** - Every decision is auditable
|
||||
//! 4. **Policy bundle reference** - All decisions reference signed governance
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use prime_radiant::execution::{
|
||||
//! Action, ActionExecutor, CoherenceGate, EnergySnapshot,
|
||||
//! LaneThresholds, PolicyBundleRef,
|
||||
//! };
|
||||
//!
|
||||
//! // Create gate with thresholds
|
||||
//! let gate = CoherenceGate::new(
|
||||
//! LaneThresholds::default(),
|
||||
//! Duration::from_secs(5),
|
||||
//! PolicyBundleRef::placeholder(),
|
||||
//! );
|
||||
//!
|
||||
//! // Create executor
|
||||
//! let executor = ActionExecutor::with_defaults(gate);
|
||||
//!
|
||||
//! // Execute action
|
||||
//! let energy = EnergySnapshot::new(0.1, 0.05, action.scope().clone());
|
||||
//! let result = executor.execute(&action, &energy);
|
||||
//!
|
||||
//! // Result always includes witness
|
||||
//! assert!(result.witness.verify_integrity());
|
||||
//! ```
|
||||
//!
|
||||
//! ## Module Structure
|
||||
//!
|
||||
//! - [`action`] - Action trait and related types for external side effects
|
||||
//! - [`gate`] - Coherence gate with threshold-based gating logic
|
||||
//! - [`ladder`] - Compute lane enum and escalation logic
|
||||
//! - [`executor`] - Action executor with mandatory witness creation
|
||||
|
||||
pub mod action;
|
||||
pub mod executor;
|
||||
pub mod gate;
|
||||
pub mod ladder;
|
||||
|
||||
// Re-export primary types for convenient access
|
||||
pub use action::{
|
||||
Action, ActionError, ActionId, ActionImpact, ActionMetadata, ActionResult, BoxedAction,
|
||||
ExecutionContext, ScopeId,
|
||||
};
|
||||
|
||||
pub use gate::{
|
||||
CoherenceGate, EnergyHistory, EnergySnapshot, GateDecision, PolicyBundleRef, WitnessId,
|
||||
WitnessRecord,
|
||||
};
|
||||
|
||||
pub use ladder::{ComputeLane, EscalationReason, LaneThresholds, LaneTransition, ThresholdError};
|
||||
|
||||
pub use executor::{
|
||||
ActionExecutor, ActionResultBuilder, ExecutionResult, ExecutionStats, ExecutorConfig,
|
||||
ExecutorStats, HumanReviewItem,
|
||||
};
|
||||
|
||||
/// Prelude module for convenient imports.
|
||||
pub mod prelude {
|
||||
pub use super::{
|
||||
Action, ActionError, ActionExecutor, ActionId, ActionImpact, ActionMetadata, ActionResult,
|
||||
CoherenceGate, ComputeLane, EnergySnapshot, EscalationReason, ExecutionContext,
|
||||
ExecutionResult, ExecutorConfig, GateDecision, LaneThresholds, PolicyBundleRef, ScopeId,
|
||||
WitnessId, WitnessRecord,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
// Integration test action
|
||||
struct IntegrationTestAction {
|
||||
scope: ScopeId,
|
||||
metadata: ActionMetadata,
|
||||
}
|
||||
|
||||
impl IntegrationTestAction {
|
||||
fn new(scope: &str) -> Self {
|
||||
Self {
|
||||
scope: ScopeId::new(scope),
|
||||
metadata: ActionMetadata::new("IntegrationTest", "Test action", "test"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Action for IntegrationTestAction {
|
||||
type Output = String;
|
||||
type Error = ActionError;
|
||||
|
||||
fn scope(&self) -> &ScopeId {
|
||||
&self.scope
|
||||
}
|
||||
|
||||
fn impact(&self) -> ActionImpact {
|
||||
ActionImpact::low()
|
||||
}
|
||||
|
||||
fn metadata(&self) -> &ActionMetadata {
|
||||
&self.metadata
|
||||
}
|
||||
|
||||
fn execute(&self, ctx: &ExecutionContext) -> Result<String, ActionError> {
|
||||
Ok(format!(
|
||||
"Executed in {:?} lane, energy: {:.3}",
|
||||
ctx.assigned_lane, ctx.current_energy
|
||||
))
|
||||
}
|
||||
|
||||
fn content_hash(&self) -> [u8; 32] {
|
||||
let hash = blake3::hash(format!("test:{}", self.scope.as_str()).as_bytes());
|
||||
let mut result = [0u8; 32];
|
||||
result.copy_from_slice(hash.as_bytes());
|
||||
result
|
||||
}
|
||||
|
||||
fn make_rollback_not_supported_error() -> ActionError {
|
||||
ActionError::RollbackNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_low_energy() {
|
||||
let gate = CoherenceGate::new(
|
||||
LaneThresholds::default(),
|
||||
Duration::from_secs(5),
|
||||
PolicyBundleRef::placeholder(),
|
||||
);
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
let action = IntegrationTestAction::new("users.123");
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
|
||||
let result = executor.execute(&action, &energy);
|
||||
|
||||
assert!(result.result.is_ok());
|
||||
assert_eq!(result.decision.lane, ComputeLane::Reflex);
|
||||
assert!(result.witness.verify_integrity());
|
||||
assert!(result.result.unwrap().contains("Reflex"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_escalation() {
|
||||
let gate = CoherenceGate::new(
|
||||
LaneThresholds::new(0.1, 0.3, 0.6),
|
||||
Duration::from_secs(5),
|
||||
PolicyBundleRef::placeholder(),
|
||||
);
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
let action = IntegrationTestAction::new("trades.456");
|
||||
let energy = EnergySnapshot::new(0.4, 0.25, action.scope.clone());
|
||||
|
||||
let result = executor.execute(&action, &energy);
|
||||
|
||||
assert!(result.result.is_ok());
|
||||
assert!(result.decision.lane >= ComputeLane::Retrieval);
|
||||
assert!(result.decision.is_escalated());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_denial() {
|
||||
let gate = CoherenceGate::new(
|
||||
LaneThresholds::new(0.1, 0.3, 0.6),
|
||||
Duration::from_secs(5),
|
||||
PolicyBundleRef::placeholder(),
|
||||
);
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
let action = IntegrationTestAction::new("critical.789");
|
||||
let energy = EnergySnapshot::new(0.9, 0.85, action.scope.clone());
|
||||
|
||||
let result = executor.execute(&action, &energy);
|
||||
|
||||
assert!(result.result.is_err());
|
||||
assert!(!result.decision.allow);
|
||||
assert_eq!(result.decision.lane, ComputeLane::Human);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_witness_chain() {
|
||||
let gate = CoherenceGate::new(
|
||||
LaneThresholds::default(),
|
||||
Duration::from_secs(5),
|
||||
PolicyBundleRef::placeholder(),
|
||||
);
|
||||
let executor = ActionExecutor::with_defaults(gate);
|
||||
|
||||
// Execute multiple actions
|
||||
let mut witnesses = Vec::new();
|
||||
for i in 0..3 {
|
||||
let action = IntegrationTestAction::new(&format!("scope.{}", i));
|
||||
let energy = EnergySnapshot::new(0.1, 0.05, action.scope.clone());
|
||||
let result = executor.execute(&action, &energy);
|
||||
witnesses.push(result.witness);
|
||||
}
|
||||
|
||||
// Verify chain
|
||||
assert!(witnesses[0].previous_witness.is_none());
|
||||
assert_eq!(witnesses[1].previous_witness, Some(witnesses[0].id.clone()));
|
||||
assert_eq!(witnesses[2].previous_witness, Some(witnesses[1].id.clone()));
|
||||
|
||||
// All witnesses should have valid integrity
|
||||
for witness in &witnesses {
|
||||
assert!(witness.verify_integrity());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lane_budget_ordering() {
|
||||
// Verify that lane latency budgets increase with lane number
|
||||
let lanes = [
|
||||
ComputeLane::Reflex,
|
||||
ComputeLane::Retrieval,
|
||||
ComputeLane::Heavy,
|
||||
ComputeLane::Human,
|
||||
];
|
||||
|
||||
for window in lanes.windows(2) {
|
||||
assert!(window[0].latency_budget_us() < window[1].latency_budget_us());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scope_hierarchy() {
|
||||
let global = ScopeId::global();
|
||||
let parent = ScopeId::new("users");
|
||||
let child = ScopeId::path(&["users", "123", "profile"]);
|
||||
|
||||
assert!(global.is_parent_of(&parent));
|
||||
assert!(global.is_parent_of(&child));
|
||||
assert!(parent.is_parent_of(&child));
|
||||
assert!(!child.is_parent_of(&parent));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_impact_risk_scores() {
|
||||
let impacts = [
|
||||
ActionImpact::minimal(),
|
||||
ActionImpact::low(),
|
||||
ActionImpact::medium(),
|
||||
ActionImpact::high(),
|
||||
ActionImpact::critical(),
|
||||
];
|
||||
|
||||
// Risk scores should generally increase
|
||||
for window in impacts.windows(2) {
|
||||
assert!(
|
||||
window[0].risk_score() <= window[1].risk_score(),
|
||||
"Risk scores should increase: {:?} vs {:?}",
|
||||
window[0].risk_score(),
|
||||
window[1].risk_score()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user