Skip to main content

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}