Skip to main content

xchecker_phase_api/
lib.rs

1//! Phase trait system for orchestrating spec generation workflows
2//!
3//! This module defines the core Phase trait and related types that enable
4//! the structured execution of spec generation phases with separated concerns.
5//!
6//! # Purpose
7//!
8//! This crate provides the shared contract between the orchestrator and phase
9//! implementations. It contains minimal types needed for phase execution
10//! without introducing circular dependencies.
11
12use anyhow::Result;
13use std::collections::HashMap;
14use std::path::PathBuf;
15use std::sync::Arc;
16
17pub use xchecker_packet::{BudgetUsage, Packet};
18use xchecker_redaction::SecretRedactor;
19use xchecker_selectors::Selectors;
20use xchecker_status::artifact::Artifact;
21pub use xchecker_utils::types::PhaseId;
22
23/// Represents the next step to take after a phase completes
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum NextStep {
26    /// Continue to the next phase in the normal flow
27    Continue,
28    /// Rewind to a previous phase (used by fixup system)
29    Rewind { to: PhaseId },
30    /// Complete the entire workflow
31    #[allow(dead_code)] // Reserved for workflow completion signaling
32    Complete,
33}
34
35/// Context information passed to phases during execution
36#[derive(Debug, Clone)]
37pub struct PhaseContext {
38    /// Unique identifier for the spec being processed
39    pub spec_id: String,
40    /// Base directory for the spec artifacts
41    pub spec_dir: PathBuf,
42    /// Configuration and runtime parameters
43    pub config: HashMap<String, String>,
44    /// Available artifacts from previous phases
45    #[allow(dead_code)] // Reserved for cross-phase artifact references
46    pub artifacts: Vec<String>,
47    /// Content selectors for packet building (from config)
48    ///
49    /// If `Some`, phases should use these selectors when building packets.
50    /// If `None`, phases fall back to built-in selector defaults.
51    pub selectors: Option<Selectors>,
52    /// Enable strict validation for phase outputs.
53    ///
54    /// When `true`, validation failures (meta-summaries, too-short output,
55    /// missing required sections) become hard errors that fail the phase.
56    /// When `false`, validation issues are logged as warnings only.
57    pub strict_validation: bool,
58    /// Secret redactor for any user-facing output emitted during phase execution.
59    ///
60    /// This is built once from the effective configuration and threaded through to ensure
61    /// configured extra/ignore patterns are applied consistently.
62    pub redactor: Arc<SecretRedactor>,
63}
64
65/// Metadata about phase execution
66#[derive(Debug, Clone, Default)]
67#[allow(dead_code)] // Metadata fields reserved for receipts and diagnostics
68pub struct PhaseMetadata {
69    /// BLAKE3 hash of the packet used for this phase
70    pub packet_hash: Option<String>,
71    /// Budget usage information
72    pub budget_used: Option<BudgetUsage>,
73    /// Duration of phase execution in milliseconds
74    pub duration_ms: Option<u64>,
75}
76
77/// Result of executing a phase
78#[derive(Debug, Clone)]
79pub struct PhaseResult {
80    /// Artifacts produced by the phase
81    pub artifacts: Vec<Artifact>,
82    /// What should happen next in the workflow
83    pub next_step: NextStep,
84    /// Additional metadata about the phase execution
85    #[allow(dead_code)] // Metadata reserved for structured receipts
86    pub metadata: PhaseMetadata,
87}
88
89/// Core trait that all workflow phases must implement
90///
91/// This trait separates concerns into three distinct operations:
92/// - `prompt()`: Generate the prompt for Claude
93/// - `make_packet()`: Prepare context packet for Claude
94/// - `postprocess()`: Process Claude's response into artifacts
95pub trait Phase {
96    /// Returns the unique identifier for this phase
97    fn id(&self) -> PhaseId;
98
99    /// Returns the phases that must complete before this phase can run
100    fn deps(&self) -> &'static [PhaseId];
101
102    /// Returns whether this phase can be resumed from a partial state
103    #[allow(dead_code)] // Trait interface for resumable phases
104    fn can_resume(&self) -> bool;
105
106    /// Generate the prompt text for Claude CLI
107    ///
108    /// This method creates the specific prompt that will be sent to Claude
109    /// for this phase, based on the current context and available artifacts.
110    fn prompt(&self, ctx: &PhaseContext) -> String;
111
112    /// Create a packet of context information for Claude
113    ///
114    /// This method builds the context packet that will be included with
115    /// the prompt, selecting and organizing relevant files and information.
116    fn make_packet(&self, ctx: &PhaseContext) -> Result<Packet>;
117
118    /// Process Claude's raw response into structured artifacts
119    ///
120    /// This method takes Claude's raw output and converts it into the
121    /// appropriate artifacts for this phase, handling any necessary
122    /// parsing and validation.
123    fn postprocess(&self, raw: &str, ctx: &PhaseContext) -> Result<PhaseResult>;
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_next_step_variants() {
132        let continue_step = NextStep::Continue;
133        assert!(matches!(continue_step, NextStep::Continue));
134
135        let rewind_step = NextStep::Rewind {
136            to: PhaseId::Requirements,
137        };
138        assert!(matches!(rewind_step, NextStep::Rewind { .. }));
139
140        let complete_step = NextStep::Complete;
141        assert!(matches!(complete_step, NextStep::Complete));
142    }
143
144    #[test]
145    fn test_phase_context_creation() {
146        let ctx = PhaseContext {
147            spec_id: "test-spec".to_string(),
148            spec_dir: PathBuf::from("/tmp/test"),
149            config: HashMap::new(),
150            artifacts: Vec::new(),
151            selectors: None,
152            strict_validation: false,
153            redactor: Arc::new(SecretRedactor::default()),
154        };
155
156        assert_eq!(ctx.spec_id, "test-spec");
157        assert!(!ctx.strict_validation);
158    }
159
160    #[test]
161    fn test_phase_metadata_default() {
162        let metadata = PhaseMetadata::default();
163        assert_eq!(metadata.packet_hash, None);
164        assert!(metadata.budget_used.is_none());
165        assert_eq!(metadata.duration_ms, None);
166    }
167}