Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

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

View File

@@ -0,0 +1,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());
}
}

View 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);
}
}

View 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);
}
}

View 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());
}
}

View 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()
);
}
}
}