use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use anyhow::bail;
use clap::Subcommand;
use crate::verify::{CheckKind, CheckPlan};
#[derive(Debug, Subcommand)]
pub(crate) enum CheckCommand {
Quick {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Commit {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Gate {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Prove {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Regression {
#[command(subcommand)]
command: RegressionCommand,
},
}
#[derive(Debug, Subcommand)]
pub(crate) enum RegressionCommand {
Capture {
#[arg(long)]
base: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Diff {
#[arg(long)]
base: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
pub(crate) fn dispatch(cmd: CheckCommand) -> anyhow::Result<()> {
use std::io::Write;
let (kind, path) = match cmd {
CheckCommand::Regression { command } => return dispatch_regression(command),
CheckCommand::Quick { path } => (CheckKind::Quick, path),
CheckCommand::Commit { path } => (CheckKind::Commit, path),
CheckCommand::Gate { path } => (CheckKind::Gate, path),
CheckCommand::Prove { path } => (CheckKind::Prove, path),
};
let root = crate::root::find(path, &crate::root::default_markers())?;
let cfg = crate::coverage_store::load_config(&root)?;
match crate::verify::resolve_check(&cfg, kind) {
CheckPlan::Noop(note) => {
writeln!(std::io::stdout(), "{note}")?;
#[expect(
clippy::disallowed_methods,
reason = "the check verb forwards a terminal exit status; an owned no-op exits 0 (design §5.4)"
)]
{
std::process::exit(0);
}
}
CheckPlan::Empty(k) => bail!(
"[verification].{} is empty — set a non-empty argv in {}",
k.key(),
crate::dtoml::DOCTRINE_TOML
),
CheckPlan::Run(argv) => run_proxy(&root, &argv, kind),
}
}
fn dispatch_regression(cmd: RegressionCommand) -> anyhow::Result<()> {
match cmd {
RegressionCommand::Capture { base, path } => {
let root = crate::root::find(path, &crate::root::default_markers())?;
crate::regression_run::run_capture(&root, &base)
}
RegressionCommand::Diff { base, path } => {
let root = crate::root::find(path, &crate::root::default_markers())?;
let code = crate::regression_run::run_diff(&root, &base)?;
#[expect(
clippy::disallowed_methods,
reason = "the regression diff verb forwards its gate verdict as the terminal exit code (INV-7)"
)]
{
std::process::exit(code);
}
}
}
}
fn run_proxy(root: &Path, argv: &[String], kind: CheckKind) -> anyhow::Result<()> {
let Some((program, args)) = argv.split_first() else {
bail!("internal: empty check argv (resolve_check INV-2 violated)");
};
let status = Command::new(program)
.args(args)
.current_dir(root)
.status()
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
anyhow::anyhow!(
"`{program}` not found — set [verification].{} in {}",
kind.key(),
crate::dtoml::DOCTRINE_TOML
)
} else {
anyhow::Error::new(e).context(format!("failed to spawn `{program}`"))
}
})?;
#[expect(
clippy::disallowed_methods,
reason = "true exit forwarding (CR-F5): the proxied child's code/signal is the verb's terminal status (design §5.4)"
)]
{
std::process::exit(exit_code(status));
}
}
fn exit_code(status: ExitStatus) -> i32 {
if let Some(code) = status.code() {
return code;
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status.signal().map_or(1, |s| 128 + s)
}
#[cfg(not(unix))]
{
1
}
}