use crate::agent::CliError;
use saccade_core::batch::{self, Options, Section};
use std::path::PathBuf;
#[derive(clap::Args)]
pub(crate) struct Args {
source: PathBuf,
#[arg(long)]
out: PathBuf,
#[arg(long,value_parser=["analyze-media","inspect","compare","text-quality","tofu","watermark","mask-metrics"],conflicts_with="options")]
section: Vec<String>,
#[arg(long)]
options: Option<PathBuf>,
#[arg(long)]
reference_dir: Option<PathBuf>,
#[arg(long, default_value_t=2,value_parser=clap::value_parser!(u64).range(1..=16),conflicts_with="options")]
concurrency: u64,
#[arg(long, default_value_t=30000,value_parser=clap::value_parser!(u64).range(1..=300000),conflicts_with="options")]
timeout_ms: u64,
#[arg(long)]
json: bool,
}
#[derive(clap::Args)]
pub(crate) struct ProbeArgs {
path: PathBuf,
thumbnail: PathBuf,
#[arg(long)]
json: bool,
}
pub(crate) fn probe(a: ProbeArgs) -> Result<u8, CliError> {
crate::media_cmd::emit(&batch::probe(&a.path, &a.thumbnail)?, a.json)
}
pub(crate) fn run(a: Args) -> Result<u8, CliError> {
if crate::signed_approval::required() {
return Err(CliError::new(
"approval_consumer_unsupported",
"signed policy refuses subprocess measurement workflows without policy propagation",
));
}
let options = if let Some(p) = a.options {
serde_json::from_slice(&saccade_core::evidence_quality::read(&p, 65536)?)?
} else {
Options {
sections: if a.section.is_empty() {
Options::default().sections
} else {
a.section
.into_iter()
.map(|command| Section {
command,
args: vec![],
})
.collect()
},
concurrency: a.concurrency as usize,
timeout_ms: a.timeout_ms,
}
};
if a.source.is_dir() {
let source = saccade_core::paths::canonicalize(&a.source)
.map_err(|e| CliError::io(e.to_string()))?;
let out = saccade_core::run::normalise_path(&a.out);
if out.starts_with(source) {
return Err(CliError::usage("batch output is inside an input directory"));
}
}
let inputs = batch::intake(&a.source, a.reference_dir.as_deref())?;
let rows = batch::run(
&std::env::current_exe().map_err(|e| CliError::io(e.to_string()))?,
&inputs,
&options,
&a.out,
)?;
let failed = rows.iter().any(|r| {
!matches!(
r["status"].as_str(),
Some("ok" | "duplicate-basename" | "skipped")
)
});
let mut counts = std::collections::BTreeMap::new();
for row in &rows {
*counts
.entry(row["status"].as_str().unwrap_or("partial"))
.or_insert(0usize) += 1;
}
crate::media_cmd::emit(
&serde_json::json!({"schema":batch::RESULT_SCHEMA,"rows":rows.len(),"counts":counts,"out":a.out,"manifest":a.out.join(saccade_core::manifest::MANIFEST_FILE)}),
a.json,
)?;
Ok(if failed { 4 } else { 0 })
}