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}