vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Frozen enforcement contract.

use crate::spec::{Finding as SpecFinding, FindingLocation};

/// Boxed structured finding returned by an [`EnforceGate`].
///
/// Using a boxed trait object lets the registry store heterogeneous
/// enforcer types in a single slice while preserving the frozen
/// `Finding` contract (source, message, fix_hint, location).
pub type Finding = Box<dyn SpecFinding>;

/// One conformance enforcement unit.
///
/// Every gate in the conform immune system implements `EnforceGate`. The
/// trait is intentionally tiny and object-safe so that adding a new gate is a
/// single-file operation with no boilerplate beyond the logic that actually
/// checks the invariant.
pub trait EnforceGate: Send + Sync {
    /// Stable snake_case enforcer id.
    ///
    /// This string appears in CI logs, coverage reports, and certificate
    /// documents. It must never change after the enforcer is first
    /// committed, because downstream consumers parse it to route
    /// findings and compute coverage metrics.
    fn id(&self) -> &'static str;

    /// Human-readable gate name.
    fn name(&self) -> &'static str;

    /// Run this enforcer against immutable enforcement context.
    ///
    /// Returns an empty vector when the invariant holds. Each returned
    /// finding is a hard failure and must include a `Fix:`-prefixed hint so
    /// that a contributor knows exactly what change will make the gate pass.
    fn run(&self, ctx: &crate::enforce::EnforceCtx<'_>) -> Vec<Finding>;
}

/// Object-safe adapter used by the generated registry.
///
/// The build scanner emits a flat slice of `&'static dyn RegisteredEnforcer`
/// so that the registry can be iterated without knowing the concrete
/// enforcer types. This trait is automatically implemented for every
/// `T: EnforceGate` via a blanket impl.
pub trait RegisteredEnforcer: Send + Sync {
    /// Stable snake_case enforcer id.
    ///
    /// Delegates to [`Enforcer::ID`] for types that implement the base
    /// trait. This method lets the registry iterate enforcers without
    /// monomorphizing over each concrete type.
    fn id(&self) -> &'static str;

    /// Human-readable gate name.
    fn name(&self) -> &'static str;

    /// Run this enforcer against immutable enforcement context.
    ///
    /// Delegates to [`EnforceGate::run`] for types that implement the
    /// base trait. The registry calls this method in a uniform loop
    /// across all registered gates.
    fn run_registered(&self, ctx: &crate::enforce::EnforceCtx<'_>) -> Vec<Finding>;
}

impl<T> RegisteredEnforcer for T
where
    T: EnforceGate,
{
    fn id(&self) -> &'static str {
        EnforceGate::id(self)
    }

    fn name(&self) -> &'static str {
        EnforceGate::name(self)
    }

    fn run_registered(&self, ctx: &crate::enforce::EnforceCtx<'_>) -> Vec<Finding> {
        EnforceGate::run(self, ctx)
    }
}

/// Aggregated finding used when legacy checks emit multiple string findings.
///
/// Some gates (especially the layer orchestrators) produce a Vec<String>
/// rather than a single structured finding. `EnforcerFinding` bundles those
/// strings into one boxed `Finding` that still satisfies the frozen contract.
#[derive(Debug)]
pub struct EnforcerFinding {
    source: &'static str,
    messages: Vec<String>,
}

impl EnforcerFinding {
    /// Build an aggregate finding for one enforcer.
    ///
    /// The `source` should be the enforcer's stable id. At least one
    /// message should contain a `Fix:` prefix so that the aggregate
    /// `fix_hint()` can surface an actionable remedy.
    #[inline]
    pub fn new(source: &'static str, messages: Vec<String>) -> Self {
        Self { source, messages }
    }
}

impl SpecFinding for EnforcerFinding {
    fn source(&self) -> &'static str {
        self.source
    }

    fn message(&self) -> String {
        self.messages.join("\n")
    }

    fn fix_hint(&self) -> String {
        self.messages
            .iter()
            .find(|message| message.contains("Fix:"))
            .cloned()
            .unwrap_or_else(|| format!("Fix: repair {} enforcement findings.", self.source))
    }

    fn location(&self) -> Option<FindingLocation> {
        None
    }
}

/// Build a boxed aggregate finding.
///
/// This is a convenience wrapper around `EnforcerFinding::new` that
/// returns the boxed type expected by the `EnforceGate` contract. It is
/// used by layer orchestrators to turn a vector of string messages into a
/// single structured finding.
#[inline]
pub fn aggregate_finding(source: &'static str, messages: Vec<String>) -> Finding {
    Box::new(EnforcerFinding::new(source, messages))
}

/// Convert string findings into the frozen gate result shape.
///
/// Returns an empty vector when `messages` is empty and one aggregate finding
/// otherwise. This helper eliminates the repetitive `if messages.is_empty()`
/// check at the end of every gate that collects string findings.
#[inline]
pub fn finding_result(source: &'static str, messages: Vec<String>) -> Vec<Finding> {
    if messages.is_empty() {
        Vec::new()
    } else {
        vec![aggregate_finding(source, messages)]
    }
}