mod check;
mod executor_defaults;
mod exit_code_parse;
mod json_path_resolve;
mod tool_evidence;
pub use check::verify_against_spec;
pub use executor_defaults::{
DEFAULT_COMMAND_EXIT_CODE, apply_executor_kind_acceptance_defaults,
default_exit_code_for_build_execution_description, effective_plan_step_acceptance,
};
pub use exit_code_parse::parse_exit_code_from_combined_output;
pub use json_path_resolve::resolve_json_path_value;
pub use tool_evidence::verify_plan_step_acceptance_for_tool_message;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyOutcome {
Pass,
Fail { reason: String },
}
impl VerifyOutcome {
pub fn is_pass(&self) -> bool {
matches!(self, VerifyOutcome::Pass)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FileResolveKind {
#[default]
AbsolutizeRelative,
WorkspaceJoin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExitCodePolicy {
#[default]
DefaultZeroIfMissing,
LenientIfUnparsed,
}
#[derive(Debug, Clone, Default)]
pub struct AcceptanceSpec {
pub expect_exit_code: Option<i32>,
pub exit_code_policy: ExitCodePolicy,
pub expect_stdout_contains: Option<String>,
pub expect_stderr_contains: Option<String>,
pub expect_combined_output_contains: Vec<String>,
pub combined_match_case_insensitive: bool,
pub expect_file_exists: Vec<String>,
pub expect_json_path_equals: Option<crate::cm_agent::plan_artifact::JsonPathEqualsRule>,
pub expect_http_status: Option<u16>,
pub file_resolve: FileResolveKind,
}
impl AcceptanceSpec {
pub fn is_empty(&self) -> bool {
self.expect_exit_code.is_none()
&& self.expect_stdout_contains.is_none()
&& self.expect_stderr_contains.is_none()
&& self.expect_combined_output_contains.is_empty()
&& self.expect_file_exists.is_empty()
&& self.expect_json_path_equals.is_none()
&& self.expect_http_status.is_none()
}
pub fn requires_tool_evidence(&self) -> bool {
self.expect_exit_code.is_some()
|| self
.expect_stdout_contains
.as_ref()
.is_some_and(|s| !s.trim().is_empty())
|| self
.expect_stderr_contains
.as_ref()
.is_some_and(|s| !s.trim().is_empty())
|| self.expect_json_path_equals.is_some()
|| self.expect_http_status.is_some()
}
}
impl From<&crate::cm_agent::plan_artifact::PlanStepAcceptance> for AcceptanceSpec {
fn from(a: &crate::cm_agent::plan_artifact::PlanStepAcceptance) -> Self {
let mut files = Vec::new();
if let Some(ref p) = a.expect_file_exists
&& !p.trim().is_empty()
{
files.push(p.clone());
}
Self {
expect_exit_code: a.expect_exit_code,
exit_code_policy: ExitCodePolicy::DefaultZeroIfMissing,
expect_stdout_contains: a.expect_stdout_contains.clone(),
expect_stderr_contains: a.expect_stderr_contains.clone(),
expect_combined_output_contains: Vec::new(),
combined_match_case_insensitive: false,
expect_file_exists: files,
expect_json_path_equals: a.expect_json_path_equals.clone(),
expect_http_status: a.expect_http_status,
file_resolve: FileResolveKind::AbsolutizeRelative,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct AcceptanceEvidence<'a> {
pub tool_name: &'a str,
pub tool_output: &'a str,
pub stdout: &'a str,
pub stderr: &'a str,
pub tool_error: Option<&'a crate::cm_tools::tool_result::ToolError>,
pub fallback_exit_code: Option<i32>,
pub workspace_root: &'a std::path::Path,
pub file_resolve: FileResolveKind,
pub combined_text_override: Option<&'a str>,
}