use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub use crate::lock::{DriftPair, LockDrift};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PhaseId {
Requirements,
Design,
Tasks,
Review,
Fixup,
Final,
}
impl PhaseId {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Requirements => "requirements",
Self::Design => "design",
Self::Tasks => "tasks",
Self::Review => "review",
Self::Fixup => "fixup",
Self::Final => "final",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Priority {
Upstream,
High,
Medium,
Low,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileType {
Yaml,
Markdown,
Text,
}
impl FileType {
#[must_use]
pub fn from_extension(ext: &str) -> Self {
match ext.to_lowercase().as_str() {
"yaml" | "yml" => Self::Yaml,
"md" | "markdown" => Self::Markdown,
_ => Self::Text,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PermissionMode {
Plan,
Auto,
Block,
}
impl PermissionMode {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Plan => "plan",
Self::Auto => "auto",
Self::Block => "block",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OutputFormat {
StreamJson,
Text,
}
impl OutputFormat {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::StreamJson => "stream-json",
Self::Text => "text",
}
}
}
pub use xchecker_runner::RunnerMode;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LlmInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_used: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens_input: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens_output: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timed_out: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub budget_exhausted: Option<bool>,
}
impl LlmInfo {
#[must_use]
pub fn for_budget_exhaustion() -> Self {
Self {
provider: None,
model_used: None,
tokens_input: None,
tokens_output: None,
timed_out: None,
timeout_seconds: None,
budget_exhausted: Some(true),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Receipt {
pub schema_version: String,
pub emitted_at: DateTime<Utc>,
pub spec_id: String,
pub phase: String,
pub xchecker_version: String,
pub claude_cli_version: String,
pub model_full_name: String,
pub model_alias: Option<String>,
pub canonicalization_version: String,
pub canonicalization_backend: String,
pub flags: HashMap<String, String>,
pub runner: String,
pub runner_distro: Option<String>,
pub packet: PacketEvidence,
pub outputs: Vec<FileHash>,
pub exit_code: i32,
pub error_kind: Option<ErrorKind>,
pub error_reason: Option<String>,
pub stderr_tail: Option<String>,
pub stderr_redacted: Option<String>,
pub warnings: Vec<String>,
pub fallback_used: Option<bool>,
pub diff_context: Option<u32>,
pub llm: Option<LlmInfo>,
pub pipeline: Option<PipelineInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(any(test, feature = "test-utils"), derive(strum::VariantNames))]
pub enum ErrorKind {
CliArgs,
PacketOverflow,
SecretDetected,
LockHeld,
PhaseTimeout,
ClaudeFailure,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PacketEvidence {
pub files: Vec<FileEvidence>,
pub max_bytes: usize,
pub max_lines: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileEvidence {
pub path: String,
pub range: Option<String>,
pub blake3_pre_redaction: String,
pub priority: Priority,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileHash {
pub path: String,
pub blake3_canonicalized: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusOutput {
pub schema_version: String,
pub emitted_at: DateTime<Utc>,
pub runner: String,
pub runner_distro: Option<String>,
pub fallback_used: bool,
pub canonicalization_version: String,
pub canonicalization_backend: String,
pub artifacts: Vec<ArtifactInfo>,
pub last_receipt_path: String,
pub effective_config: std::collections::BTreeMap<String, ConfigValue>,
pub lock_drift: Option<LockDrift>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pending_fixups: Option<PendingFixupsSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoctorOutput {
pub schema_version: String,
pub emitted_at: DateTime<Utc>,
pub ok: bool,
pub checks: Vec<DoctorCheck>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_stats: Option<crate::cache::CacheStats>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoctorCheck {
pub name: String,
pub status: CheckStatus,
pub details: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(any(test, feature = "test-utils"), derive(strum::VariantNames))]
pub enum CheckStatus {
Pass,
Warn,
Fail,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArtifactInfo {
pub path: String,
pub blake3_first8: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigValue {
pub value: serde_json::Value,
pub source: ConfigSource,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(any(test, feature = "test-utils"), derive(strum::VariantNames))]
pub enum ConfigSource {
Cli,
Env,
Config,
Programmatic,
Default,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PendingFixupsSummary {
pub targets: u32,
pub est_added: u32,
pub est_removed: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineInfo {
pub execution_strategy: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpecOutput {
pub schema_version: String,
pub spec_id: String,
pub phases: Vec<PhaseInfo>,
pub config_summary: SpecConfigSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseInfo {
pub phase_id: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_run: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpecConfigSummary {
pub execution_strategy: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
pub spec_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusJsonOutput {
pub schema_version: String,
pub spec_id: String,
pub phase_statuses: Vec<PhaseStatusInfo>,
pub pending_fixups: u32,
pub has_errors: bool,
pub strict_validation: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub artifacts: Vec<ArtifactInfo>,
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty", default)]
pub effective_config: std::collections::BTreeMap<String, ConfigValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock_drift: Option<LockDrift>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseStatusInfo {
pub phase_id: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub receipt_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResumeJsonOutput {
pub schema_version: String,
pub spec_id: String,
pub phase: String,
pub current_inputs: CurrentInputs,
pub next_steps: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CurrentInputs {
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub available_artifacts: Vec<String>,
pub spec_exists: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_completed_phase: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceStatusJsonOutput {
pub schema_version: String,
pub workspace_name: String,
pub workspace_path: String,
pub specs: Vec<WorkspaceSpecStatus>,
pub summary: WorkspaceStatusSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceSpecStatus {
pub spec_id: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub tags: Vec<String>,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_phase: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_activity: Option<DateTime<Utc>>,
pub pending_fixups: u32,
pub has_errors: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceStatusSummary {
pub total_specs: u32,
pub successful_specs: u32,
pub failed_specs: u32,
pub pending_specs: u32,
pub not_started_specs: u32,
pub stale_specs: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceHistoryJsonOutput {
pub schema_version: String,
pub spec_id: String,
pub timeline: Vec<HistoryEntry>,
pub metrics: HistoryMetrics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
pub phase: String,
pub timestamp: DateTime<Utc>,
pub exit_code: i32,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens_input: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tokens_output: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fixup_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryMetrics {
pub total_executions: u32,
pub successful_executions: u32,
pub failed_executions: u32,
pub total_tokens_input: u64,
pub total_tokens_output: u64,
pub total_fixups: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub first_execution: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_execution: Option<DateTime<Utc>>,
}