Skip to main content

xchecker_utils/
types.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5// Re-export lock types for use in status output
6pub use crate::lock::{DriftPair, LockDrift};
7
8/// Phase identifiers for the spec generation workflow.
9///
10/// `PhaseId` represents the different phases in xchecker's spec generation pipeline.
11/// Phases execute in a defined order with dependencies between them.
12///
13/// # Phase Order
14///
15/// The standard workflow progresses through phases in this order:
16///
17/// ```text
18/// Requirements → Design → Tasks → Review → Fixup → Final
19/// ```
20///
21/// # Dependencies
22///
23/// - `Requirements`: No dependencies (starting phase)
24/// - `Design`: Requires `Requirements` to complete successfully
25/// - `Tasks`: Requires `Design` to complete successfully
26/// - `Review`: Requires `Tasks` to complete successfully
27/// - `Fixup`: Requires `Review` to complete successfully
28/// - `Final`: Requires `Fixup` to complete successfully
29///
30/// # Example
31///
32/// ```rust
33/// use xchecker_utils::types::PhaseId;
34///
35/// let phase = PhaseId::Requirements;
36/// assert_eq!(phase.as_str(), "requirements");
37///
38/// // PhaseId is Copy, so it can be used multiple times
39/// let phase2 = phase;
40/// assert_eq!(phase, phase2);
41/// ```
42///
43/// # Serialization
44///
45/// `PhaseId` serializes to its string representation (e.g., `"requirements"`, `"design"`).
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub enum PhaseId {
48    /// Requirements phase: transforms rough ideas into structured EARS requirements.
49    Requirements,
50    /// Design phase: creates architecture and component design from requirements.
51    Design,
52    /// Tasks phase: generates actionable implementation tasks from design.
53    Tasks,
54    /// Review phase: validates and refines the generated artifacts.
55    Review,
56    /// Fixup phase: applies code changes proposed by the LLM.
57    Fixup,
58    /// Final phase: completes the workflow and generates final artifacts.
59    Final,
60}
61
62impl PhaseId {
63    /// Returns the string representation of the phase.
64    ///
65    /// This is the canonical lowercase name used in receipts, status output,
66    /// and CLI commands.
67    ///
68    /// # Example
69    ///
70    /// ```rust
71    /// use xchecker_utils::types::PhaseId;
72    ///
73    /// assert_eq!(PhaseId::Requirements.as_str(), "requirements");
74    /// assert_eq!(PhaseId::Design.as_str(), "design");
75    /// assert_eq!(PhaseId::Tasks.as_str(), "tasks");
76    /// ```
77    #[must_use]
78    pub const fn as_str(&self) -> &'static str {
79        match self {
80            Self::Requirements => "requirements",
81            Self::Design => "design",
82            Self::Tasks => "tasks",
83            Self::Review => "review",
84            Self::Fixup => "fixup",
85            Self::Final => "final",
86        }
87    }
88}
89
90/// Priority levels for content selection in packet building
91#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
92pub enum Priority {
93    /// Upstream *.core.yaml files - never evicted
94    Upstream,
95    /// High priority files (SPEC/ADR/REPORT)
96    High,
97    /// Medium priority files (README/SCHEMA)
98    Medium,
99    /// Low priority files (misc)
100    Low,
101}
102
103/// File types for canonicalization and processing
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105pub enum FileType {
106    Yaml,
107    Markdown,
108    Text,
109}
110
111impl FileType {
112    /// Determine file type from extension
113    #[must_use]
114    pub fn from_extension(ext: &str) -> Self {
115        match ext.to_lowercase().as_str() {
116            "yaml" | "yml" => Self::Yaml,
117            "md" | "markdown" => Self::Markdown,
118            _ => Self::Text,
119        }
120    }
121}
122
123/// Permission modes for Claude CLI tool usage
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125pub enum PermissionMode {
126    /// Plan mode - show what would be done
127    Plan,
128    /// Auto mode - automatically approve tool usage
129    Auto,
130    /// Block mode - block all tool usage
131    Block,
132}
133
134impl PermissionMode {
135    #[must_use]
136    pub const fn as_str(&self) -> &'static str {
137        match self {
138            Self::Plan => "plan",
139            Self::Auto => "auto",
140            Self::Block => "block",
141        }
142    }
143}
144
145/// Output formats supported by Claude CLI
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147pub enum OutputFormat {
148    /// Structured JSON streaming format (preferred)
149    StreamJson,
150    /// Plain text format (fallback)
151    Text,
152}
153
154impl OutputFormat {
155    #[must_use]
156    pub const fn as_str(&self) -> &'static str {
157        match self {
158            Self::StreamJson => "stream-json",
159            Self::Text => "text",
160        }
161    }
162}
163
164/// Runner modes for cross-platform Claude CLI execution.
165pub use xchecker_runner::RunnerMode;
166
167/// LLM metadata for receipts (wires ClaudeResponse fields into receipts)
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169pub struct LlmInfo {
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub provider: Option<String>,
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub model_used: Option<String>,
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub tokens_input: Option<u64>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub tokens_output: Option<u64>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub timed_out: Option<bool>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub timeout_seconds: Option<u64>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub budget_exhausted: Option<bool>,
184}
185
186impl LlmInfo {
187    /// Create an LlmInfo for budget exhaustion errors
188    ///
189    /// This is used when an LLM invocation fails due to budget exhaustion,
190    /// allowing us to record the budget_exhausted flag in the receipt even
191    /// though there's no successful LlmResult.
192    #[must_use]
193    pub fn for_budget_exhaustion() -> Self {
194        Self {
195            provider: None,
196            model_used: None,
197            tokens_input: None,
198            tokens_output: None,
199            timed_out: None,
200            timeout_seconds: None,
201            budget_exhausted: Some(true),
202        }
203    }
204}
205
206/// Enhanced receipt structure for multi-file support and full auditability
207/// Records comprehensive information about phase execution including Claude CLI details
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct Receipt {
210    /// Schema version for this receipt format
211    pub schema_version: String,
212    /// RFC3339 UTC timestamp when the receipt was emitted
213    pub emitted_at: DateTime<Utc>,
214    /// Unique identifier for the spec being processed
215    pub spec_id: String,
216    /// Phase that was executed
217    pub phase: String,
218    /// Version of xchecker that generated this receipt
219    pub xchecker_version: String,
220    /// Version of Claude CLI that was used
221    pub claude_cli_version: String,
222    /// Full model name that was actually used
223    pub model_full_name: String,
224    /// Model alias that was requested (if any)
225    pub model_alias: Option<String>,
226    /// Version of the canonicalization algorithm used
227    pub canonicalization_version: String,
228    /// Backend used for canonicalization (e.g., "jcs-rfc8785")
229    pub canonicalization_backend: String,
230    /// CLI flags and configuration used
231    pub flags: HashMap<String, String>,
232    /// Runner mode used for Claude CLI execution ("native" | "wsl")
233    pub runner: String,
234    /// WSL distribution name if runner is "wsl"
235    pub runner_distro: Option<String>,
236    /// Evidence of packet construction
237    pub packet: PacketEvidence,
238    /// BLAKE3 hashes of canonicalized outputs (sorted by path before emission)
239    pub outputs: Vec<FileHash>,
240    /// Exit code from the phase execution (0 = success)
241    pub exit_code: i32,
242    /// Error kind for non-zero exits
243    pub error_kind: Option<ErrorKind>,
244    /// Brief error reason for non-zero exits
245    pub error_reason: Option<String>,
246    /// Standard error tail (limited to 2 KiB)
247    pub stderr_tail: Option<String>,
248    /// Redacted standard error output (limited to 2 KiB)
249    pub stderr_redacted: Option<String>,
250    /// Warnings encountered during execution
251    pub warnings: Vec<String>,
252    /// Whether fallback to text format was used
253    pub fallback_used: Option<bool>,
254    /// Diff context lines (0 when --unidiff-zero is enabled)
255    pub diff_context: Option<u32>,
256    /// LLM metadata for receipts (V11+ multi-provider support)
257    pub llm: Option<LlmInfo>,
258    /// Pipeline configuration metadata (V11+)
259    pub pipeline: Option<PipelineInfo>,
260}
261
262/// Error kinds for receipt error tracking
263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
264#[serde(rename_all = "snake_case")]
265#[cfg_attr(any(test, feature = "test-utils"), derive(strum::VariantNames))]
266pub enum ErrorKind {
267    CliArgs,
268    PacketOverflow,
269    SecretDetected,
270    LockHeld,
271    PhaseTimeout,
272    ClaudeFailure,
273    Unknown,
274}
275
276/// Evidence of packet construction for auditability
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct PacketEvidence {
279    /// List of files included in the packet
280    pub files: Vec<FileEvidence>,
281    /// Maximum bytes allowed in packet
282    pub max_bytes: usize,
283    /// Maximum lines allowed in packet
284    pub max_lines: usize,
285}
286
287/// Evidence of a single file's inclusion in the packet
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct FileEvidence {
290    /// Path to the file relative to project root
291    pub path: String,
292    /// Optional range of lines included (e.g., "L1-L80")
293    pub range: Option<String>,
294    /// BLAKE3 hash of the file content before redaction
295    pub blake3_pre_redaction: String,
296    /// Priority level of this file
297    pub priority: Priority,
298}
299
300/// Represents a file hash in the receipt
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct FileHash {
303    /// Path to the file relative to the spec directory
304    pub path: String,
305    /// BLAKE3 hash of the canonicalized content
306    pub blake3_canonicalized: String,
307}
308
309/// Status output for a spec, matching `schemas/status.v1.json`.
310///
311/// `StatusOutput` provides comprehensive status information about a spec's current state,
312/// including artifacts, configuration, and any detected drift from locked values.
313///
314/// This is a stable public type. Changes in 1.x releases are additive only.
315///
316/// # Schema
317///
318/// This type conforms to `schemas/status.v1.json` and is emitted using JCS (RFC 8785)
319/// canonicalization for stable, deterministic JSON output.
320///
321/// # Example
322///
323/// ```rust
324/// use chrono::Utc;
325/// use std::collections::BTreeMap;
326/// use xchecker_utils::types::{ConfigValue, StatusOutput};
327///
328/// let status = StatusOutput {
329///     schema_version: "1".to_string(),
330///     emitted_at: Utc::now(),
331///     runner: "native".to_string(),
332///     runner_distro: None,
333///     fallback_used: false,
334///     canonicalization_version: "yaml-v1,md-v1".to_string(),
335///     canonicalization_backend: "jcs-rfc8785".to_string(),
336///     artifacts: Vec::new(),
337///     last_receipt_path: "receipts/latest.json".to_string(),
338///     effective_config: BTreeMap::<String, ConfigValue>::new(),
339///     lock_drift: None,
340///     pending_fixups: None,
341/// };
342///
343/// println!("Schema version: {}", status.schema_version);
344/// println!("Artifacts: {}", status.artifacts.len());
345/// ```
346///
347/// # Fields
348///
349/// - `schema_version`: Always `"1"` for this schema version
350/// - `emitted_at`: RFC3339 UTC timestamp when status was generated
351/// - `runner`: Execution mode (`"native"` or `"wsl"`)
352/// - `artifacts`: List of artifacts with BLAKE3 hashes (first 8 chars)
353/// - `effective_config`: Configuration values with source attribution
354/// - `lock_drift`: Detected differences from locked values (if any)
355/// - `pending_fixups`: Summary of pending code changes (if any)
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct StatusOutput {
358    /// Schema version for this status format (always `"1"` for v1).
359    pub schema_version: String,
360    /// RFC3339 UTC timestamp when the status was emitted.
361    pub emitted_at: DateTime<Utc>,
362    /// Runner mode used for Claude CLI execution (`"native"` or `"wsl"`).
363    pub runner: String,
364    /// WSL distribution name if runner is `"wsl"`.
365    pub runner_distro: Option<String>,
366    /// Whether fallback to text format was used during LLM invocation.
367    pub fallback_used: bool,
368    /// Version of the canonicalization algorithm used (e.g., `"yaml-v1,md-v1"`).
369    pub canonicalization_version: String,
370    /// Backend used for canonicalization (e.g., `"jcs-rfc8785"`).
371    pub canonicalization_backend: String,
372    /// Artifacts with path and `blake3_first8` hash (sorted by path).
373    pub artifacts: Vec<ArtifactInfo>,
374    /// Path to the most recent receipt file.
375    pub last_receipt_path: String,
376    /// Effective configuration with source attribution (`cli`, `config`, `programmatic`, or `default`).
377    pub effective_config: std::collections::BTreeMap<String, ConfigValue>,
378    /// Lock drift information if lockfile exists and drift is detected.
379    pub lock_drift: Option<LockDrift>,
380    /// Pending fixup summary (counts only, no file contents).
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub pending_fixups: Option<PendingFixupsSummary>,
383}
384
385/// Doctor output structure for JSON emission (schema v1)
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct DoctorOutput {
388    /// Schema version for this doctor format
389    pub schema_version: String,
390    /// RFC3339 UTC timestamp when the doctor output was emitted
391    pub emitted_at: DateTime<Utc>,
392    /// Overall health status (true if all checks pass or warn, false if any fail)
393    pub ok: bool,
394    /// Health checks performed (sorted by name before emission)
395    pub checks: Vec<DoctorCheck>,
396    /// Cache statistics (wired from InsightCache)
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub cache_stats: Option<crate::cache::CacheStats>,
399}
400
401/// Individual health check result
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct DoctorCheck {
404    /// Name of the check
405    pub name: String,
406    /// Status of the check
407    pub status: CheckStatus,
408    /// Details about the check result
409    pub details: String,
410}
411
412/// Status of a health check
413#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
414#[serde(rename_all = "snake_case")]
415#[cfg_attr(any(test, feature = "test-utils"), derive(strum::VariantNames))]
416pub enum CheckStatus {
417    Pass,
418    Warn,
419    Fail,
420}
421
422/// Artifact information for status output.
423///
424/// Represents a single artifact file with its path and truncated BLAKE3 hash.
425/// Used in [`StatusOutput`] to list all artifacts for a spec.
426///
427/// # Example
428///
429/// ```rust
430/// use xchecker_utils::types::ArtifactInfo;
431///
432/// let artifact = ArtifactInfo {
433///     path: "artifacts/00-requirements.md".to_string(),
434///     blake3_first8: "a1b2c3d4".to_string(),
435/// };
436/// ```
437#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct ArtifactInfo {
439    /// Path to the artifact relative to the spec directory.
440    pub path: String,
441    /// First 8 characters of the BLAKE3 hash of the canonicalized content.
442    pub blake3_first8: String,
443}
444
445/// Configuration value with source attribution.
446///
447/// Tracks both the value and where it came from (CLI, config file, programmatic, or defaults).
448/// Used in [`StatusOutput`] to show effective configuration.
449///
450/// # Example
451///
452/// ```rust
453/// use xchecker_utils::types::{ConfigValue, ConfigSource};
454/// use serde_json::json;
455///
456/// let config_value = ConfigValue {
457///     value: json!("haiku"),
458///     source: ConfigSource::Config,
459/// };
460/// ```
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct ConfigValue {
463    /// The configuration value as arbitrary JSON.
464    pub value: serde_json::Value,
465    /// Source of this configuration value.
466    pub source: ConfigSource,
467}
468
469/// Source of a configuration value.
470///
471/// Indicates where a configuration value originated from in the precedence chain:
472/// CLI arguments > environment variables > config file > programmatic overrides > built-in defaults.
473///
474/// # Serialization
475///
476/// Serializes to lowercase strings: `"cli"`, `"env"`, `"config"`, `"programmatic"`, `"default"`.
477///
478/// # Example
479///
480/// ```rust
481/// use xchecker_utils::types::ConfigSource;
482///
483/// let source = ConfigSource::Cli;
484/// let json = serde_json::to_string(&source).unwrap();
485/// assert_eq!(json, r#""cli""#);
486/// ```
487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
488#[serde(rename_all = "lowercase")]
489#[cfg_attr(any(test, feature = "test-utils"), derive(strum::VariantNames))]
490pub enum ConfigSource {
491    /// Value provided via CLI argument (highest precedence).
492    Cli,
493    /// Value loaded from environment variable.
494    Env,
495    /// Value loaded from configuration file.
496    Config,
497    /// Value provided programmatically (e.g., `Config::builder()`).
498    Programmatic,
499    /// Built-in default value (lowest precedence).
500    Default,
501}
502
503/// Pending fixups summary (counts only, no file contents or diffs)
504#[derive(Debug, Clone, Serialize, Deserialize)]
505pub struct PendingFixupsSummary {
506    /// Number of target files with pending fixups
507    pub targets: u32,
508    /// Estimated number of lines to be added
509    pub est_added: u32,
510    /// Estimated number of lines to be removed
511    pub est_removed: u32,
512}
513
514/// Pipeline configuration metadata (V11+)
515/// All fields are optional for backward compatibility
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct PipelineInfo {
518    /// Execution strategy used ("controlled" | "external_tool")
519    pub execution_strategy: Option<String>,
520}
521
522/// Spec output structure for JSON emission (schema spec-json.v1)
523/// Used by `xchecker spec --json` command for Claude Code integration
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct SpecOutput {
526    /// Schema version for this spec format (e.g., "spec-json.v1")
527    pub schema_version: String,
528    /// Unique identifier for the spec
529    pub spec_id: String,
530    /// List of phases with high-level metadata
531    pub phases: Vec<PhaseInfo>,
532    /// Configuration summary (paths, execution strategy, provider)
533    pub config_summary: SpecConfigSummary,
534}
535
536/// Phase information for spec output (high-level metadata only)
537#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct PhaseInfo {
539    /// Phase identifier
540    pub phase_id: String,
541    /// Phase status: "completed", "pending", "not_started"
542    pub status: String,
543    /// RFC3339 UTC timestamp of last run (if any)
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub last_run: Option<DateTime<Utc>>,
546}
547
548/// Configuration summary for spec output
549/// Excludes full artifacts and packet contents per FR-Claude Code-CLI requirements
550#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct SpecConfigSummary {
552    /// Execution strategy used ("controlled" | "external_tool")
553    pub execution_strategy: String,
554    /// LLM provider configured
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub provider: Option<String>,
557    /// Spec directory path
558    pub spec_path: String,
559}
560
561/// Status output structure for JSON emission (schema status-json.v2)
562/// Used by `xchecker status --json` command for Claude Code integration
563/// Includes artifacts with blake3_first8, effective_config, and lock_drift
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct StatusJsonOutput {
566    /// Schema version for this status format (e.g., "status-json.v2")
567    pub schema_version: String,
568    /// Unique identifier for the spec
569    pub spec_id: String,
570    /// List of phase statuses with receipt IDs
571    pub phase_statuses: Vec<PhaseStatusInfo>,
572    /// Number of pending fixups (0 if none)
573    pub pending_fixups: u32,
574    /// Whether any errors exist in the spec
575    pub has_errors: bool,
576    /// Whether strict validation mode is enabled (validation failures fail phases)
577    pub strict_validation: bool,
578    /// Artifacts with path and blake3_first8 hash (first 8 chars of BLAKE3)
579    #[serde(skip_serializing_if = "Vec::is_empty", default)]
580    pub artifacts: Vec<ArtifactInfo>,
581    /// Effective configuration with source attribution (cli/config/programmatic/default)
582    #[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty", default)]
583    pub effective_config: std::collections::BTreeMap<String, ConfigValue>,
584    /// Lock drift information if lockfile exists and drift detected
585    #[serde(skip_serializing_if = "Option::is_none")]
586    pub lock_drift: Option<LockDrift>,
587}
588
589/// Phase status information for compact status output
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct PhaseStatusInfo {
592    /// Phase identifier
593    pub phase_id: String,
594    /// Phase status: "success", "failed", "not_started"
595    pub status: String,
596    /// Receipt ID for the latest run (if any)
597    #[serde(skip_serializing_if = "Option::is_none")]
598    pub receipt_id: Option<String>,
599}
600
601/// Resume output structure for JSON emission (schema resume-json.v1)
602/// Used by `xchecker resume --json` command for Claude Code integration
603/// Per FR-Claude Code-CLI (Requirements 4.1.3): Returns resume context without full packet/artifacts
604#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct ResumeJsonOutput {
606    /// Schema version for this resume format (e.g., "resume-json.v1")
607    pub schema_version: String,
608    /// Unique identifier for the spec
609    pub spec_id: String,
610    /// Phase to resume from
611    pub phase: String,
612    /// Current inputs available for the phase (artifact names, not full contents)
613    pub current_inputs: CurrentInputs,
614    /// Next steps hint for the user/agent
615    pub next_steps: String,
616}
617
618/// Current inputs available for a phase (high-level metadata only)
619/// Excludes full packet and raw artifacts per FR-Claude Code-CLI requirements
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct CurrentInputs {
622    /// List of available artifact names (not full contents)
623    #[serde(skip_serializing_if = "Vec::is_empty", default)]
624    pub available_artifacts: Vec<String>,
625    /// Whether the spec directory exists
626    pub spec_exists: bool,
627    /// Latest completed phase (if any)
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub latest_completed_phase: Option<String>,
630}
631
632/// Workspace status output structure for JSON emission (schema workspace-status-json.v1)
633/// Used by `xchecker project status --json` command for aggregated workspace status
634/// Per FR-WORKSPACE (Requirements 4.3.4): Emits aggregated status for all specs
635#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct WorkspaceStatusJsonOutput {
637    /// Schema version for this workspace status format (e.g., "workspace-status-json.v1")
638    pub schema_version: String,
639    /// Name of the workspace
640    pub workspace_name: String,
641    /// Path to the workspace file
642    pub workspace_path: String,
643    /// Per-spec phase summaries
644    pub specs: Vec<WorkspaceSpecStatus>,
645    /// Summary counts
646    pub summary: WorkspaceStatusSummary,
647}
648
649/// Per-spec status information for workspace status output
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct WorkspaceSpecStatus {
652    /// Spec identifier
653    pub spec_id: String,
654    /// Tags associated with the spec
655    #[serde(skip_serializing_if = "Vec::is_empty", default)]
656    pub tags: Vec<String>,
657    /// Overall spec status: "success", "failed", "pending", "not_started", "stale"
658    pub status: String,
659    /// Latest completed phase (if any)
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub latest_phase: Option<String>,
662    /// RFC3339 UTC timestamp of last activity (if any)
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub last_activity: Option<DateTime<Utc>>,
665    /// Number of pending fixups for this spec
666    pub pending_fixups: u32,
667    /// Whether this spec has errors
668    pub has_errors: bool,
669}
670
671/// Summary counts for workspace status
672#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct WorkspaceStatusSummary {
674    /// Total number of specs in the workspace
675    pub total_specs: u32,
676    /// Number of specs with successful latest phase
677    pub successful_specs: u32,
678    /// Number of specs with failed latest phase
679    pub failed_specs: u32,
680    /// Number of specs with pending work (not completed all phases)
681    pub pending_specs: u32,
682    /// Number of specs that haven't been started
683    pub not_started_specs: u32,
684    /// Number of stale specs (no recent activity)
685    pub stale_specs: u32,
686}
687
688/// Workspace history output structure for JSON emission (schema workspace-history-json.v1)
689/// Used by `xchecker project history <spec-id> --json` command for spec timeline
690/// Per FR-WORKSPACE (Requirements 4.3.5): Emits timeline of phase progression
691#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct WorkspaceHistoryJsonOutput {
693    /// Schema version for this workspace history format (e.g., "workspace-history-json.v1")
694    pub schema_version: String,
695    /// Spec identifier
696    pub spec_id: String,
697    /// Timeline of phase executions
698    pub timeline: Vec<HistoryEntry>,
699    /// Aggregated metrics across all executions
700    pub metrics: HistoryMetrics,
701}
702
703/// A single entry in the spec history timeline
704#[derive(Debug, Clone, Serialize, Deserialize)]
705pub struct HistoryEntry {
706    /// Phase that was executed
707    pub phase: String,
708    /// RFC3339 UTC timestamp of execution
709    pub timestamp: DateTime<Utc>,
710    /// Exit code of the execution (0 = success)
711    pub exit_code: i32,
712    /// Whether the execution was successful
713    pub success: bool,
714    /// LLM token usage for this execution (if available)
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub tokens_input: Option<u64>,
717    /// LLM token output for this execution (if available)
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub tokens_output: Option<u64>,
720    /// Number of fixups applied in this execution (if applicable)
721    #[serde(skip_serializing_if = "Option::is_none")]
722    pub fixup_count: Option<u32>,
723    /// Model used for this execution
724    #[serde(skip_serializing_if = "Option::is_none")]
725    pub model: Option<String>,
726    /// Provider used for this execution
727    #[serde(skip_serializing_if = "Option::is_none")]
728    pub provider: Option<String>,
729}
730
731/// Aggregated metrics for spec history
732#[derive(Debug, Clone, Serialize, Deserialize)]
733pub struct HistoryMetrics {
734    /// Total number of phase executions
735    pub total_executions: u32,
736    /// Number of successful executions
737    pub successful_executions: u32,
738    /// Number of failed executions
739    pub failed_executions: u32,
740    /// Total LLM tokens consumed (input)
741    pub total_tokens_input: u64,
742    /// Total LLM tokens consumed (output)
743    pub total_tokens_output: u64,
744    /// Total fixups applied across all executions
745    pub total_fixups: u32,
746    /// First execution timestamp (if any)
747    #[serde(skip_serializing_if = "Option::is_none")]
748    pub first_execution: Option<DateTime<Utc>>,
749    /// Last execution timestamp (if any)
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub last_execution: Option<DateTime<Utc>>,
752}