saccade 0.2.7

Visual-regression CLI for renderers and graphics code, built on NVIDIA FLIP
//! Standalone arm checks and shared strict flags.
#[derive(Clone, Copy, clap::ValueEnum)]
pub(crate) enum Compare {
    MappedOnly,
    All,
}
impl From<Compare> for saccade_core::arms::CompareMode {
    fn from(value: Compare) -> Self {
        match value {
            Compare::MappedOnly => Self::MappedOnly,
            Compare::All => Self::All,
        }
    }
}
use crate::agent::CliError;
use std::path::PathBuf;
#[derive(clap::Args, Clone, Default)]
pub(crate) struct StrictArgs {
    /// Permit both unreached arms only under an identical criterion and observation.
    #[arg(long = "allow-unreached")]
    pub allow_unreached: Vec<String>,
    /// Refuse verdicts for incomplete or mismatched producer identity.
    #[arg(long)]
    pub require_valid_arms: bool,
    /// Generic TOML/JSON mapping from producer fields and sibling records.
    #[arg(long)]
    pub fingerprint_map: Option<PathBuf>,
    /// Explicit ignored metadata tokens, echoed in arm validation output.
    #[arg(long = "arm-ignore")]
    pub arm_ignore: Vec<String>,
}
impl StrictArgs {
    pub(crate) fn apply(&self, opts: &mut saccade_core::meta::MetaOptions) {
        opts.require_valid_arms |= self.require_valid_arms;
        if let Some(map) = &self.fingerprint_map {
            opts.fingerprint_map = Some(map.clone());
        }
        opts.ignore.extend(self.arm_ignore.iter().cloned());
        opts.allow_unreached
            .extend(self.allow_unreached.iter().cloned());
    }
}
#[derive(clap::Args)]
pub(crate) struct Args {
    #[command(subcommand)]
    operation: Operation,
}
#[derive(clap::Subcommand)]
enum Operation {
    /// Check two capture records, sidecars, images or capture directories.
    Check {
        a: PathBuf,
        b: PathBuf,
        /// Allowed difference matching destination or mapped source: exact key, dotted prefix, suffix or glob; repeat or comma-separate.
        #[arg(long, value_delimiter = ',')]
        vary: Vec<String>,
        /// Permit intentionally unreached captures with exactly matching observations.
        #[arg(long = "allow-unreached")]
        allow_unreached: Vec<String>,
        /// Explicit exception matching destination or mapped source, echoed even if unmatched; repeat or comma-separate.
        #[arg(long, value_delimiter = ',')]
        ignore: Vec<String>,
        #[arg(long)]
        fingerprint_map: Option<PathBuf>,
        /// Fingerprint record limit in bytes (default 16 MiB; hard ceiling 64 MiB); overrides map.
        #[arg(long)]
        max_record_bytes: Option<u64>,
        /// Override the map's field selection (mapped-only requires a map).
        #[arg(long, value_enum)]
        compare: Option<Compare>,
        #[arg(long)]
        config: Option<PathBuf>,
        #[arg(long)]
        meta_name: Option<String>,
        #[arg(long)]
        json: bool,
    },
}
pub(crate) fn emit(check: &saccade_core::arms::Check, json: bool) -> Result<(), CliError> {
    let text = if json {
        serde_json::to_string(check)?
    } else {
        format!(
            "{}\nUnmapped: {} keys ({} listed{}); outcomes: {} keys ({} listed{}).",
            serde_json::to_string_pretty(check)?,
            check.unmapped.count,
            check.unmapped.keys.len(),
            if check.unmapped.truncated {
                ", truncated"
            } else {
                ""
            },
            check.outcomes.count,
            check.outcomes.keys.len(),
            if check.outcomes.truncated {
                ", truncated"
            } else {
                ""
            }
        )
    };
    crate::emit(&format!("{text}\n"))
}
pub(crate) fn run(args: Args) -> Result<u8, CliError> {
    match args.operation {
        Operation::Check {
            a,
            b,
            vary,
            allow_unreached,
            ignore,
            fingerprint_map,
            max_record_bytes,
            compare,
            config,
            meta_name,
            json,
        } => {
            let mut cfg = crate::load_config(config.as_deref())?;
            cfg.meta.compare = compare.map(Into::into);
            if let Some(limit) = max_record_bytes {
                cfg.meta.max_record_bytes = Some(limit);
            }
            cfg.meta.intended.extend(vary);
            cfg.meta.allow_unreached.extend(allow_unreached);
            cfg.meta.ignore.extend(ignore);
            if let Some(map) = fingerprint_map {
                cfg.meta.fingerprint_map = Some(map);
            }
            if let Some(name) = meta_name {
                cfg.meta.name = name;
            }
            let result = saccade_core::arms::validate_paths(&a, &b, &cfg)?;
            emit(&result, json)?;
            Ok(result.exit_code)
        }
    }
}

pub(crate) fn annotate(value: &mut serde_json::Value, opts: &saccade_core::meta::MetaOptions) {
    if opts.require_valid_arms {
        value["arm_validation"] = serde_json::json!({"result":"valid_comparison","ignore":opts.ignore,"vary":opts.intended});
    }
}
pub(crate) fn success_text(opts: &saccade_core::meta::MetaOptions) -> Result<(), CliError> {
    if opts.require_valid_arms {
        crate::emit(&format!(
            "valid_comparison: ignored {}; intended {}\n",
            serde_json::to_string(&opts.ignore)?,
            serde_json::to_string(&opts.intended)?
        ))?;
    }
    Ok(())
}