vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! Gauntlet runner — executes defendants against the target op's law set.
//!
//! Given a Defendant (a deliberately-wrong CPU reference) and the target
//! op's declared laws, the gauntlet runs the law checker against the
//! Defendant and records which laws fired violations. A Defendant
//! **escapes** when none of its declared `fails_laws` fingerprints
//! appear in the violation list — either the law checker is broken
//! (LAW 1) or the declared law set is too weak to notice the sabotage.
//!
//! A Defendant **is caught** when at least one of its declared
//! `fails_laws` fingerprints appears in the violation list. "Caught" is
//! the desired outcome for a Prosecutor: every Defendant must be
//! caught or the Prosecutor's suite is insufficient.
//!
//! The gauntlet also tracks **over-catches** — laws not in
//! `fails_laws` that nevertheless fired. Over-catches are INFORMATIONAL
//! — they signal that the sabotage was more aggressive than intended.
//! They are not failures.

use crate::adversarial::defender::{Defendant, DefendantCatalog};
use crate::pipeline::certify::CertificateStrength;
use crate::proof::algebra::checker::{verify_laws, verify_laws_witnessed};
use crate::spec::law::{canonical_law_id, AlgebraicLaw};
use crate::OpSpec;

/// The outcome of running one defendant against one op's law set.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GauntletFinding {
    /// Defendant identifier.
    pub defendant_id: String,
    /// Target op id.
    pub target_op_id: String,
    /// Laws that fired a violation when checked against the defendant.
    pub laws_violated: Vec<String>,
    /// Laws that fired but were not in the defendant's `fails_laws` list.
    pub over_catches: Vec<String>,
    /// Laws declared in `fails_laws` that did NOT fire — the gap.
    pub escaped_laws: Vec<String>,
    /// True when the defendant disagreed with the target op at its concrete
    /// expected witness.
    pub witness_failed: bool,
    /// True when every declared `fails_laws` entry fired. False when one
    /// or more slipped past — the defendant escaped detection.
    pub caught: bool,
    /// Classification for findings that are not real defendant executions.
    pub status: GauntletStatus,
}

/// Classification for one gauntlet finding.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GauntletStatus {
    /// The defendant was executed against a registered target op.
    Executed,
    /// The defendant entry is malformed and must be fixed before it can prove anything.
    Malformed {
        /// Actionable reason the defendant was rejected.
        reason: String,
    },
    /// The defendant catalog targets an op that is not registered in the supplied specs.
    SkippedMissingTarget,
    /// The defendant was caught but its sabotage trips too many undeclared laws.
    Imprecise {
        /// Actionable reason the defendant should be narrowed.
        reason: String,
    },
}

/// A full gauntlet run across every defendant for a set of op specs.
#[derive(Debug, Clone, Default)]
pub struct GauntletReport {
    /// Per-defendant findings.
    pub findings: Vec<GauntletFinding>,
}

impl GauntletReport {
    /// Defendants that escaped detection — the Prosecutor findings.
    #[inline]
    pub fn escaped(&self) -> Vec<&GauntletFinding> {
        self.findings
            .iter()
            .filter(|f| {
                !f.caught
                    && matches!(
                        f.status,
                        GauntletStatus::Executed | GauntletStatus::Imprecise { .. }
                    )
            })
            .collect()
    }

    /// Defendants that were caught.
    #[inline]
    pub fn caught(&self) -> Vec<&GauntletFinding> {
        self.findings.iter().filter(|f| f.caught).collect()
    }
}

/// Return every accepted fingerprint for a law.
///
/// The canonical custom-law id includes the predicate address so registry
/// drift can detect predicate substitution. Defender manifests are human-authored
/// TOML and still use the stable `custom(name)` target label, so gauntlet
/// matching accepts that legacy alias while preserving the stronger canonical id.
fn law_fingerprints(law: &AlgebraicLaw) -> Vec<String> {
    let mut fingerprints = vec![canonical_law_id(law)];
    if let AlgebraicLaw::Custom { name, .. } = law {
        fingerprints.push(format!("custom({name})"));
    }
    fingerprints
}

/// Run one defendant against the declared laws of its target op and
/// report the finding.
///
/// Backwards-compat shim for legacy callers — uses u8-exhaustive
/// verification (65k witnesses for binary ops). New callers should
/// prefer [`run_defendant_with_strength`] so that `Standard` and
/// `Legendary` runs scale up to 1M / 100M witnesses per law as the
/// persisted certificate promises.
#[inline]
pub fn run_defendant(
    defendant: &Defendant,
    spec_cpu_fn: fn(&[u8]) -> Vec<u8>,
    declared_laws: &[AlgebraicLaw],
    is_binary: bool,
) -> GauntletFinding {
    run_defendant_with_strength(
        defendant,
        spec_cpu_fn,
        declared_laws,
        is_binary,
        CertificateStrength::FastCheck,
    )
}

/// Run one defendant against the declared laws of its target op at the
/// given certificate strength. Per audit L.1.25: when the caller
/// requests `Standard` (1M) or `Legendary` (100M), the gauntlet must
/// honor that witness count instead of silently capping at u8
/// exhaustive (65k binary / 256 unary). Otherwise a mutated CPU
/// reference that passes u8-exhaustive but fails at wider u32 witnesses
/// would escape under a `Standard` run even though the certificate
/// promises 1M u32 witnesses were tried.
#[inline]
pub fn run_defendant_with_strength(
    defendant: &Defendant,
    spec_cpu_fn: fn(&[u8]) -> Vec<u8>,
    declared_laws: &[AlgebraicLaw],
    is_binary: bool,
    strength: CertificateStrength,
) -> GauntletFinding {
    let expected: Vec<String> = defendant
        .fails_laws
        .iter()
        .map(|s| (*s).to_string())
        .collect();
    if expected.is_empty() && defendant.expected_witness.is_empty() {
        return GauntletFinding {
            defendant_id: defendant.id.to_string(),
            target_op_id: defendant.target_op_id.to_string(),
            laws_violated: Vec::new(),
            over_catches: Vec::new(),
            escaped_laws: Vec::new(),
            witness_failed: false,
            caught: false,
            status: GauntletStatus::Malformed {
                reason: "defendant declares no fails_laws and no expected_witness. Fix: add at least one law fingerprint or concrete witness.".to_string(),
            },
        };
    }

    let checked_laws: Vec<AlgebraicLaw> = declared_laws.to_vec();

    // FastCheck runs the fast u8-exhaustive path (≤65k witnesses).
    // Standard/Legendary escalate to random u32 witnessed verification
    // so the persisted certificate's declared witness count is
    // actually sampled during the adversarial gauntlet.
    let results = if matches!(strength, CertificateStrength::FastCheck) {
        verify_laws(
            defendant.target_op_id,
            defendant.broken_fn,
            &checked_laws,
            is_binary,
        )
    } else {
        verify_laws_witnessed(
            defendant.target_op_id,
            defendant.broken_fn,
            &checked_laws,
            is_binary,
            strength.witness_count(),
        )
    };

    // Map each verified law back to its fingerprint so we can compare
    // against the defendant's `fails_laws` list.
    let mut violated_fingerprints: Vec<String> = Vec::new();
    for (law, result) in checked_laws.iter().zip(results.iter()) {
        if result.violation.is_some() {
            violated_fingerprints.extend(law_fingerprints(law));
        }
    }
    violated_fingerprints.sort();
    violated_fingerprints.dedup();

    let escaped_laws: Vec<String> = expected
        .iter()
        .filter(|e| !violated_fingerprints.contains(*e))
        .cloned()
        .collect();

    let over_catches: Vec<String> = violated_fingerprints
        .iter()
        .filter(|v| !expected.contains(v))
        .cloned()
        .collect();

    let witness_failed = defendant.expected_witness.iter().any(|(a, b)| {
        let mut input = Vec::with_capacity(if is_binary { 8 } else { 4 });
        input.extend_from_slice(&a.to_le_bytes());
        if is_binary {
            input.extend_from_slice(&b.to_le_bytes());
        }
        (defendant.broken_fn)(&input) != spec_cpu_fn(&input)
    });

    // A defendant is killed if its declared law violations fire or its
    // concrete witness differs from the real target CPU reference. The
    // witness path is important for operations whose current declared law
    // set is intentionally weak, such as a full-u32 bounded law.
    let caught = (!expected.is_empty() && escaped_laws.is_empty()) || witness_failed;

    let status = if over_catches.len() > expected.len().saturating_mul(2).max(2) {
        GauntletStatus::Imprecise {
            reason: format!(
                "defendant tripped {} undeclared laws for {} declared laws. Fix: narrow the sabotage or declare the additional targeted laws.",
                over_catches.len(),
                expected.len()
            ),
        }
    } else {
        GauntletStatus::Executed
    };

    GauntletFinding {
        defendant_id: defendant.id.to_string(),
        target_op_id: defendant.target_op_id.to_string(),
        laws_violated: violated_fingerprints,
        over_catches,
        escaped_laws,
        witness_failed,
        caught,
        status,
    }
}

/// Determine binary vs unary arity from the target op's signature, by
/// looking it up in the passed-in spec list. Caller provides the specs.
#[inline]
pub fn run_gauntlet(catalogs: &[DefendantCatalog], specs: &[OpSpec]) -> GauntletReport {
    let mut report = GauntletReport::default();

    for catalog in catalogs {
        let Some(spec) = specs.iter().find(|s| s.id == catalog.target_op_id) else {
            // Target op is not registered; record a skipped defendant
            // for each entry so the report is complete.
            for defendant in &catalog.defendants {
                report.findings.push(GauntletFinding {
                    defendant_id: defendant.id.to_string(),
                    target_op_id: defendant.target_op_id.to_string(),
                    laws_violated: Vec::new(),
                    over_catches: Vec::new(),
                    escaped_laws: defendant
                        .fails_laws
                        .iter()
                        .map(|s| (*s).to_string())
                        .collect(),
                    witness_failed: false,
                    caught: false,
                    status: GauntletStatus::SkippedMissingTarget,
                });
            }
            continue;
        };
        let is_binary = spec.signature.inputs.len() == 2;
        for defendant in &catalog.defendants {
            let finding = run_defendant(defendant, spec.cpu_fn, &spec.laws, is_binary);
            report.findings.push(finding);
        }
    }

    report
}