use camino::Utf8PathBuf;
use chrono::Utc;
use std::collections::HashMap;
use xchecker_utils::error::XCheckerError;
use xchecker_utils::types::{ErrorKind, PacketEvidence, PhaseId, Receipt};
use super::ReceiptManager;
pub(super) const fn error_to_exit_code_and_kind(error: &XCheckerError) -> (i32, ErrorKind) {
use xchecker_utils::error::PhaseError;
match error {
XCheckerError::Config(_) => (2, ErrorKind::CliArgs),
XCheckerError::PacketOverflow { .. } => (7, ErrorKind::PacketOverflow),
XCheckerError::SecretDetected { .. } => (8, ErrorKind::SecretDetected),
XCheckerError::ConcurrentExecution { .. } => (9, ErrorKind::LockHeld),
XCheckerError::Lock(_) => (9, ErrorKind::LockHeld),
XCheckerError::Phase(phase_err) => match phase_err {
PhaseError::Timeout { .. } => (10, ErrorKind::PhaseTimeout),
PhaseError::InvalidTransition { .. } => (2, ErrorKind::CliArgs),
PhaseError::DependencyNotSatisfied { .. } => (2, ErrorKind::CliArgs),
_ => (1, ErrorKind::Unknown),
},
XCheckerError::Claude(_) => (70, ErrorKind::ClaudeFailure),
XCheckerError::Runner(_) => (70, ErrorKind::ClaudeFailure),
_ => (1, ErrorKind::Unknown),
}
}
#[allow(dead_code)] pub fn write_error_receipt_and_exit(
error: &XCheckerError,
spec_id: &str,
phase: PhaseId,
spec_base_path: &Utf8PathBuf,
) -> ! {
let (exit_code, error_kind) = error_to_exit_code_and_kind(error);
let error_reason = error.to_string();
let redacted_error_reason = xchecker_redaction::redact_user_string(&error_reason);
let receipt_manager = ReceiptManager::new(spec_base_path);
let receipt = Receipt {
schema_version: "1".to_string(),
emitted_at: Utc::now(),
spec_id: spec_id.to_string(),
phase: phase.as_str().to_string(),
xchecker_version: env!("CARGO_PKG_VERSION").to_string(),
claude_cli_version: "unknown".to_string(), model_full_name: "unknown".to_string(),
model_alias: None,
canonicalization_version: "yaml-v1,md-v1".to_string(),
canonicalization_backend: "jcs-rfc8785".to_string(),
flags: HashMap::new(),
runner: "unknown".to_string(),
runner_distro: None,
packet: PacketEvidence {
files: vec![],
max_bytes: 0,
max_lines: 0,
},
outputs: vec![],
exit_code,
error_kind: Some(error_kind),
error_reason: Some(redacted_error_reason.clone()),
stderr_tail: None,
stderr_redacted: None,
warnings: vec![],
fallback_used: None,
diff_context: None,
llm: None, pipeline: None, };
if let Err(write_err) = receipt_manager.write_receipt(&receipt) {
eprintln!("Warning: Failed to write error receipt: {write_err}");
eprintln!("Original error: {redacted_error_reason}");
}
std::process::exit(exit_code);
}
impl ReceiptManager {
#[must_use]
#[allow(dead_code)] #[allow(clippy::too_many_arguments)]
pub fn create_error_receipt(
&self,
spec_id: &str,
phase: PhaseId,
error: &XCheckerError,
xchecker_version: &str,
claude_cli_version: &str,
model_full_name: &str,
model_alias: Option<String>,
flags: HashMap<String, String>,
packet: PacketEvidence,
stderr_tail: Option<String>,
stderr_redacted: Option<String>,
warnings: Vec<String>,
fallback_used: Option<bool>,
runner: &str,
runner_distro: Option<String>,
diff_context: Option<u32>,
pipeline: Option<xchecker_utils::types::PipelineInfo>,
) -> Receipt {
let (exit_code, error_kind) = error_to_exit_code_and_kind(error);
let error_reason = error.to_string();
self.create_receipt(
spec_id,
phase,
exit_code,
vec![], xchecker_version,
claude_cli_version,
model_full_name,
model_alias,
flags,
packet,
stderr_tail,
stderr_redacted,
warnings,
fallback_used,
runner,
runner_distro,
Some(error_kind),
Some(error_reason),
diff_context,
pipeline,
)
}
}