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}