vyre-conform 0.1.0

Conformance suite for vyre backends — proves byte-identical output to CPU reference
Documentation
//! H9 — coverage harness.
//!
//! Wraps `crate::spec::registry::verify::verify_specs` and returns a structured
//! report with counts of ops per category, total laws declared, and any
//! coverage errors found.

use std::collections::HashMap;

use crate::spec::op_registry;
use crate::spec::registry::error::CoverageError;
use crate::spec::registry::verify::verify_specs;
use vyre_spec::Category;

/// Structured coverage report.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoverageReport {
    /// `true` when the registry passes all coverage checks.
    pub passed: bool,
    /// Every coverage error found, empty on success.
    pub errors: Vec<CoverageError>,
    /// Total number of ops in the registry.
    pub total_ops: usize,
    /// Number of ops grouped by category string.
    pub ops_per_category: HashMap<String, usize>,
    /// Total number of algebraic laws declared across all ops.
    pub total_laws: usize,
}

/// Iterate the full op registry and produce a structured coverage report.
#[inline]
pub fn spec_coverage_check() -> CoverageReport {
    let specs = op_registry::all_specs();
    coverage_report_for_specs(&specs)
}

/// Produce a coverage report for a caller-supplied spec slice.
///
/// This is used by regression tests to exercise harness-only checks without
/// mutating the global registry.
#[inline]
pub fn coverage_report_for_specs(specs: &[crate::OpSpec]) -> CoverageReport {
    let mut errors = match verify_specs(specs) {
        Ok(()) => Vec::new(),
        Err(e) => e,
    };
    errors.extend(mandatory_field_errors(specs));
    let mut ops_per_category = HashMap::new();
    let mut total_laws = 0;

    for spec in specs {
        let cat_key = category_label(&spec.category);
        *ops_per_category.entry(cat_key).or_insert(0) += 1;
        total_laws += spec.laws.len();
    }

    CoverageReport {
        passed: errors.is_empty(),
        errors,
        total_ops: specs.len(),
        ops_per_category,
        total_laws,
    }
}

fn mandatory_field_errors(specs: &[crate::OpSpec]) -> Vec<CoverageError> {
    let mut errors = Vec::new();
    for spec in specs {
        if matches!(&spec.category, Category::A { composition_of } if composition_of.is_empty()) {
            errors.push(CoverageError::UnclassifiedCategory {
                op_id: spec.id.to_string(),
            });
        }
        if spec.archetypes.is_empty() {
            errors.push(CoverageError::InvalidOracle {
                op_id: spec.id.to_string(),
                message: "missing archetypes. Fix: declare at least one archetype id for generator coverage.".to_string(),
            });
        }
        if spec.mutation_sensitivity.is_empty() {
            errors.push(CoverageError::InvalidOracle {
                op_id: spec.id.to_string(),
                message: "missing mutation_sensitivity. Fix: declare the mutation classes this op's tests must kill.".to_string(),
            });
        }
        if spec.spec_table.is_empty() {
            errors.push(CoverageError::InvalidOracle {
                op_id: spec.id.to_string(),
                message: "missing spec_table. Fix: add at least one load-bearing input/output row."
                    .to_string(),
            });
        }
    }
    errors
}

fn category_label(category: &Category) -> String {
    match category {
        Category::A { .. } => "A".to_string(),
        Category::C { .. } => "C".to_string(),
        _ => "unknown".to_string(),
    }
}