converge-core 2.1.2

Converge Agent OS - correctness-first, context-driven multi-agent runtime
Documentation
// Copyright 2024-2026 Reflective Labs
// SPDX-License-Identifier: MIT

//! # Promoter Capability Boundary Trait
//!
//! This module defines the capability boundary trait for promoting validated
//! proposals to Facts. Promoters take `Proposal<Validated>` (which requires proof
//! of validation) and produce `Fact` with complete audit trail.
//!
//! ## Design Philosophy
//!
//! - **Type-state enforcement:** Works with `Proposal<Validated>` which can only
//!   be created after validation. This ensures "no bypass path" at the type level.
//!
//! - **Fact immutability:** Once a `Fact` is created, it's immutable. Corrections
//!   are new events, not mutations (append-only principle).
//!
//! - **GAT async pattern:** Uses generic associated types for zero-cost async
//!   without proc macros or `async_trait`. Keeps core dependency-free.
//!
//! - **Split from validation:** Promotion is a separate capability from validation.
//!   Different authorization boundaries and audit trails.
//!
//! ## Integration with Gate Pattern
//!
//! The `Promoter` trait abstracts the promotion capability that `PromotionGate`
//! uses internally. This allows:
//! - Swapping promotion implementations (immediate, queued, consensus-based)
//! - Testing with mock promoters
//! - Distributed promotion across services
//!
//! ## Error Handling
//!
//! [`PromoterError`] implements [`CapabilityError`](super::error::CapabilityError)
//! for uniform error classification, enabling generic retry/circuit breaker logic.

use super::error::{CapabilityError, ErrorCategory};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;

use crate::gates::validation::ValidationReport;
use crate::types::{Actor, EvidenceRef, Fact, Proposal, TraceLink, Validated};

/// Boxed future type for dyn-safe trait variant.
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

// ============================================================================
// Promotion Context
// ============================================================================

/// Context for promotion operations.
///
/// Contains the metadata and evidence required for creating the promotion record.
#[derive(Debug, Clone)]
pub struct PromotionContext {
    /// Actor approving the promotion.
    pub approver: Actor,
    /// Evidence references supporting the promotion.
    pub evidence: Vec<EvidenceRef>,
    /// Trace link for audit/replay.
    pub trace: TraceLink,
}

impl PromotionContext {
    /// Create a new promotion context.
    pub fn new(approver: Actor, trace: TraceLink) -> Self {
        Self {
            approver,
            evidence: Vec::new(),
            trace,
        }
    }

    /// Add evidence to the context.
    pub fn with_evidence(mut self, evidence: Vec<EvidenceRef>) -> Self {
        self.evidence = evidence;
        self
    }

    /// Add a single evidence reference.
    pub fn with_evidence_ref(mut self, evidence: EvidenceRef) -> Self {
        self.evidence.push(evidence);
        self
    }
}

// ============================================================================
// Error Type
// ============================================================================

/// Error type for promotion operations.
///
/// Implements [`CapabilityError`] for uniform error classification.
#[derive(Debug, Clone)]
pub enum PromoterError {
    /// Validation report doesn't match proposal.
    ReportMismatch {
        /// Expected proposal ID from report.
        expected: String,
        /// Actual proposal ID.
        actual: String,
    },
    /// Actor not authorized to promote.
    Unauthorized {
        /// Actor that attempted promotion.
        actor: String,
        /// Reason for denial.
        reason: String,
    },
    /// Proposal already promoted.
    AlreadyPromoted {
        /// Proposal ID.
        proposal_id: String,
        /// Existing fact ID.
        fact_id: String,
    },
    /// Promoter service unavailable.
    Unavailable {
        /// Error message.
        message: String,
    },
    /// Operation timed out.
    Timeout {
        /// Time elapsed before timeout.
        elapsed: Duration,
        /// Configured deadline.
        deadline: Duration,
    },
    /// Storage error during fact creation.
    StorageError {
        /// Error message.
        message: String,
    },
    /// Internal promoter error.
    Internal {
        /// Error message.
        message: String,
    },
}

impl std::fmt::Display for PromoterError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ReportMismatch { expected, actual } => {
                write!(
                    f,
                    "validation report mismatch: expected proposal '{}', got '{}'",
                    expected, actual
                )
            }
            Self::Unauthorized { actor, reason } => {
                write!(f, "actor '{}' not authorized: {}", actor, reason)
            }
            Self::AlreadyPromoted {
                proposal_id,
                fact_id,
            } => {
                write!(
                    f,
                    "proposal '{}' already promoted to fact '{}'",
                    proposal_id, fact_id
                )
            }
            Self::Unavailable { message } => write!(f, "promoter unavailable: {}", message),
            Self::Timeout { elapsed, deadline } => {
                write!(
                    f,
                    "promotion timeout after {:?} (deadline: {:?})",
                    elapsed, deadline
                )
            }
            Self::StorageError { message } => write!(f, "storage error: {}", message),
            Self::Internal { message } => write!(f, "internal promoter error: {}", message),
        }
    }
}

impl std::error::Error for PromoterError {}

impl CapabilityError for PromoterError {
    fn category(&self) -> ErrorCategory {
        match self {
            Self::ReportMismatch { .. } => ErrorCategory::InvalidInput,
            Self::Unauthorized { .. } => ErrorCategory::Auth,
            Self::AlreadyPromoted { .. } => ErrorCategory::Conflict,
            Self::Unavailable { .. } => ErrorCategory::Unavailable,
            Self::Timeout { .. } => ErrorCategory::Timeout,
            Self::StorageError { .. } => ErrorCategory::Internal,
            Self::Internal { .. } => ErrorCategory::Internal,
        }
    }

    fn is_transient(&self) -> bool {
        matches!(
            self,
            Self::Unavailable { .. } | Self::Timeout { .. } | Self::StorageError { .. }
        )
    }

    fn is_retryable(&self) -> bool {
        // Transient errors are retryable
        // Internal errors may also be retryable (temporary service issues)
        // AlreadyPromoted is NOT retryable (idempotency check)
        self.is_transient() || matches!(self, Self::Internal { .. })
    }

    fn retry_after(&self) -> Option<Duration> {
        // No specific retry-after for promotion errors
        None
    }
}

// ============================================================================
// Static Dispatch Trait (GAT Async Pattern)
// ============================================================================

/// Proposal promotion capability.
///
/// Promotes `Proposal<Validated>` to `Fact` with complete audit trail.
/// This trait uses the GAT async pattern for zero-cost static dispatch.
///
/// # Type-State Integration
///
/// Works with the type-state pattern established in Phase 4:
/// - Input: `Proposal<Validated>` - can only be created after validation
/// - Input: `ValidationReport` - proof that validation occurred
/// - Output: `Fact` - immutable record with promotion provenance
///
/// The type system ensures no bypass path: you cannot call `promote` without
/// first having a `Proposal<Validated>` (which requires validation).
///
/// # Example Implementation
///
/// ```ignore
/// struct ImmediatePromoter {
///     store: Arc<dyn FactStore>,
/// }
///
/// impl Promoter for ImmediatePromoter {
///     type PromoteFut<'a> = impl Future<Output = Result<Fact, PromoterError>> + Send + 'a
///     where
///         Self: 'a;
///
///     fn promote<'a>(
///         &'a self,
///         proposal: Proposal<Validated>,
///         report: &'a ValidationReport,
///         context: &'a PromotionContext,
///     ) -> Self::PromoteFut<'a> {
///         async move {
///             // Create fact and store...
///             Ok(fact)
///         }
///     }
/// }
/// ```
pub trait Promoter: Send + Sync {
    /// Associated future type for promotion.
    ///
    /// Must be `Send` to work with multi-threaded runtimes.
    type PromoteFut<'a>: Future<Output = Result<Fact, PromoterError>> + Send + 'a
    where
        Self: 'a;

    /// Promote a validated proposal to a Fact.
    ///
    /// # Arguments
    ///
    /// * `proposal` - The validated proposal to promote (consumed).
    /// * `report` - The validation report (proof of validation).
    /// * `context` - Promotion context (approver, evidence, trace).
    ///
    /// # Returns
    ///
    /// A future that resolves to the created Fact or an error.
    /// The Fact includes complete promotion provenance.
    fn promote<'a>(
        &'a self,
        proposal: Proposal<Validated>,
        report: &'a ValidationReport,
        context: &'a PromotionContext,
    ) -> Self::PromoteFut<'a>;
}

// ============================================================================
// Dyn-Safe Wrapper (Runtime Polymorphism)
// ============================================================================

/// Dyn-safe promoter for runtime polymorphism.
///
/// Use this trait when you need `dyn Trait` compatibility, such as:
/// - Storing multiple promoter types in a collection
/// - Runtime routing between different promotion strategies
/// - Plugin systems with dynamic loading
///
/// For static dispatch (better performance, no allocation), use [`Promoter`].
///
/// # Blanket Implementation
///
/// Any type implementing [`Promoter`] automatically implements [`DynPromoter`]
/// via a blanket impl that boxes the future.
pub trait DynPromoter: Send + Sync {
    /// Promote a validated proposal to a Fact.
    ///
    /// Returns a boxed future for dyn-safety.
    fn promote<'a>(
        &'a self,
        proposal: Proposal<Validated>,
        report: &'a ValidationReport,
        context: &'a PromotionContext,
    ) -> BoxFuture<'a, Result<Fact, PromoterError>>;
}

// Blanket implementation: Promoter -> DynPromoter
impl<T: Promoter> DynPromoter for T {
    fn promote<'a>(
        &'a self,
        proposal: Proposal<Validated>,
        report: &'a ValidationReport,
        context: &'a PromotionContext,
    ) -> BoxFuture<'a, Result<Fact, PromoterError>> {
        Box::pin(Promoter::promote(self, proposal, report, context))
    }
}