ralph_workflow/reducer/event/pipeline_event.rs
1//! The top-level `PipelineEvent` enum.
2
3use serde::{Deserialize, Serialize};
4
5use super::agent::AgentEvent;
6use super::development::DevelopmentEvent;
7use super::review::ReviewEvent;
8use super::types::{
9 AwaitingDevFixEvent, CheckpointTrigger, CommitEvent, LifecycleEvent, PlanningEvent,
10 PromptInputEvent, RebaseEvent,
11};
12
13/// Pipeline events representing all state transitions.
14///
15/// Events are organized into logical categories for type-safe routing
16/// to category-specific reducers. Each category has a dedicated inner enum.
17///
18/// # Event Categories
19///
20/// - `Lifecycle` - Pipeline start/stop/resume
21/// - `Planning` - Plan generation events
22/// - `Development` - Development iteration and continuation events
23/// - `Review` - Review pass and fix attempt events
24/// - `Agent` - Agent invocation and chain management events
25/// - `Rebase` - Git rebase operation events
26/// - `Commit` - Commit generation events
27/// - Miscellaneous events (context cleanup, checkpoints, finalization)
28///
29/// # Example
30///
31/// ```rust,ignore
32/// // Type-safe event construction
33/// let event = PipelineEvent::Agent(AgentEvent::InvocationStarted {
34/// role: AgentRole::Developer,
35/// agent: "claude".to_string(),
36/// model: Some("opus".to_string()),
37/// });
38///
39/// // Pattern matching routes to category handlers
40/// match event {
41/// PipelineEvent::Agent(agent_event) => reduce_agent_event(state, agent_event),
42/// // ...
43/// }
44/// ```
45///
46/// # ⚠️ FROZEN - DO NOT ADD VARIANTS ⚠️
47///
48/// This enum is **FROZEN**. Adding new top-level variants is **PROHIBITED**.
49///
50/// ## Why is this frozen?
51///
52/// `PipelineEvent` provides category-based event routing to the reducer. The existing
53/// categories (Lifecycle, Planning, Development, Review, etc.) cover all pipeline phases.
54/// Adding new top-level variants would indicate a missing architectural abstraction or
55/// an attempt to bypass phase-specific event handling.
56///
57/// ## What to do instead
58///
59/// 1. **Express events through existing categories** - Use the category enums:
60/// - `PlanningEvent` for planning phase observations
61/// - `DevelopmentEvent` for development phase observations
62/// - `ReviewEvent` for review phase observations
63/// - `CommitEvent` for commit generation observations
64/// - `AgentEvent` for agent invocation observations
65/// - `RebaseEvent` for rebase state machine transitions
66///
67/// 2. **Return errors for unrecoverable failures** - Don't create events for conditions
68/// that should terminate the pipeline. Return `Err` from the effect handler instead.
69///
70/// 3. **Extend category enums if needed** - If you truly need a new event within an
71/// existing phase, add it to that phase's category enum (e.g., add a new variant to
72/// `ReviewEvent` rather than creating a new top-level category).
73///
74/// ## Enforcement
75///
76/// The freeze policy is enforced by the `pipeline_event_is_frozen` test in this module,
77/// which will fail to compile if new variants are added. This is intentional.
78///
79/// See `LifecycleEvent` documentation for additional context on the freeze policy rationale.
80#[derive(Clone, Serialize, Deserialize, Debug)]
81pub enum PipelineEvent {
82 /// Pipeline lifecycle events (start, stop, resume).
83 Lifecycle(LifecycleEvent),
84 /// Planning phase events.
85 Planning(PlanningEvent),
86 /// Development phase events.
87 Development(DevelopmentEvent),
88 /// Review phase events.
89 Review(ReviewEvent),
90 /// Prompt input materialization events.
91 PromptInput(PromptInputEvent),
92 /// Agent invocation and chain events.
93 Agent(AgentEvent),
94 /// Rebase operation events.
95 Rebase(RebaseEvent),
96 /// Commit generation events.
97 Commit(CommitEvent),
98 /// `AwaitingDevFix` phase events.
99 AwaitingDevFix(AwaitingDevFixEvent),
100
101 // ========================================================================
102 // Miscellaneous events that don't fit a category
103 // ========================================================================
104 /// Context cleanup completed.
105 ContextCleaned,
106 /// Checkpoint saved.
107 CheckpointSaved {
108 /// What triggered the checkpoint save.
109 trigger: CheckpointTrigger,
110 },
111 /// Finalization phase started.
112 FinalizingStarted,
113 /// PROMPT.md permissions restored.
114 PromptPermissionsRestored,
115 /// Loop recovery triggered (tight loop detected and broken).
116 LoopRecoveryTriggered {
117 /// String representation of the detected loop.
118 detected_loop: String,
119 /// Number of times the loop was repeated.
120 loop_count: u32,
121 },
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 /// This test enforces the FROZEN policy on `LifecycleEvent`.
129 ///
130 /// If you're here because this test failed to compile after adding
131 /// a variant, you are violating the freeze policy. See the FROZEN
132 /// comment on `LifecycleEvent` for alternatives.
133 #[test]
134 fn lifecycle_event_is_frozen() {
135 fn exhaustive_match(e: &LifecycleEvent) -> &'static str {
136 match e {
137 LifecycleEvent::Started => "started",
138 LifecycleEvent::Resumed { .. } => "resumed",
139 LifecycleEvent::Completed => "completed",
140 LifecycleEvent::GitignoreEntriesEnsured { .. } => "gitignore_ensured",
141 // DO NOT ADD _ WILDCARD - intentionally exhaustive
142 }
143 }
144 // Just needs to compile; actual call proves exhaustiveness
145 let _ = exhaustive_match(&LifecycleEvent::Started);
146 }
147
148 /// This test enforces the FROZEN policy on `PipelineEvent`.
149 ///
150 /// If you're here because this test failed to compile after adding
151 /// a variant, you are violating the freeze policy. See the FROZEN
152 /// comment on `PipelineEvent` for alternatives.
153 #[test]
154 fn pipeline_event_is_frozen() {
155 fn exhaustive_match(e: &PipelineEvent) -> &'static str {
156 match e {
157 PipelineEvent::Lifecycle(_) => "lifecycle",
158 PipelineEvent::Planning(_) => "planning",
159 PipelineEvent::Development(_) => "development",
160 PipelineEvent::Review(_) => "review",
161 PipelineEvent::PromptInput(_) => "prompt_input",
162 PipelineEvent::Agent(_) => "agent",
163 PipelineEvent::Rebase(_) => "rebase",
164 PipelineEvent::Commit(_) => "commit",
165 PipelineEvent::AwaitingDevFix(_) => "awaiting_dev_fix",
166 PipelineEvent::ContextCleaned => "context_cleaned",
167 PipelineEvent::CheckpointSaved { .. } => "checkpoint_saved",
168 PipelineEvent::FinalizingStarted => "finalizing_started",
169 PipelineEvent::PromptPermissionsRestored => "prompt_permissions_restored",
170 PipelineEvent::LoopRecoveryTriggered { .. } => "loop_recovery_triggered",
171 // DO NOT ADD _ WILDCARD - intentionally exhaustive
172 }
173 }
174 let _ = exhaustive_match(&PipelineEvent::ContextCleaned);
175 }
176}