ricecoder_specs/
approval.rs

1//! Approval gate management for spec phase transitions
2//!
3//! Manages approval gates and enforces sequential phase progression through
4//! the spec writing workflow (requirements → design → tasks → execution).
5
6use crate::error::SpecError;
7use crate::models::{ApprovalGate, SpecPhase, SpecWritingSession};
8use chrono::Utc;
9
10/// Manages approval gates and phase transitions for spec writing sessions
11#[derive(Debug, Clone)]
12pub struct ApprovalManager;
13
14impl ApprovalManager {
15    /// Creates a new approval manager
16    pub fn new() -> Self {
17        ApprovalManager
18    }
19
20    /// Initializes approval gates for all phases in a new session
21    ///
22    /// Creates approval gates for each phase in sequential order.
23    /// All gates start in unapproved state.
24    pub fn initialize_gates() -> Vec<ApprovalGate> {
25        vec![
26            ApprovalGate {
27                phase: SpecPhase::Discovery,
28                approved: false,
29                approved_at: None,
30                approved_by: None,
31                feedback: None,
32            },
33            ApprovalGate {
34                phase: SpecPhase::Requirements,
35                approved: false,
36                approved_at: None,
37                approved_by: None,
38                feedback: None,
39            },
40            ApprovalGate {
41                phase: SpecPhase::Design,
42                approved: false,
43                approved_at: None,
44                approved_by: None,
45                feedback: None,
46            },
47            ApprovalGate {
48                phase: SpecPhase::Tasks,
49                approved: false,
50                approved_at: None,
51                approved_by: None,
52                feedback: None,
53            },
54            ApprovalGate {
55                phase: SpecPhase::Execution,
56                approved: false,
57                approved_at: None,
58                approved_by: None,
59                feedback: None,
60            },
61        ]
62    }
63
64    /// Approves the current phase in a session
65    ///
66    /// Records approval with timestamp and approver information.
67    /// Returns error if phase is already approved or if session is invalid.
68    ///
69    /// # Arguments
70    ///
71    /// * `session` - The spec writing session to approve
72    /// * `approver` - Name of the person approving
73    /// * `feedback` - Optional feedback on the phase
74    ///
75    /// # Returns
76    ///
77    /// Updated session with approval recorded, or error if approval fails
78    pub fn approve_phase(
79        session: &mut SpecWritingSession,
80        approver: &str,
81        feedback: Option<String>,
82    ) -> Result<(), SpecError> {
83        // Find the gate for the current phase
84        let gate = session
85            .approval_gates
86            .iter_mut()
87            .find(|g| g.phase == session.phase)
88            .ok_or_else(|| {
89                SpecError::InvalidFormat(format!(
90                    "No approval gate found for phase {:?}",
91                    session.phase
92                ))
93            })?;
94
95        // Record approval
96        gate.approved = true;
97        gate.approved_at = Some(Utc::now());
98        gate.approved_by = Some(approver.to_string());
99        gate.feedback = feedback;
100
101        // Update session timestamp
102        session.updated_at = Utc::now();
103
104        Ok(())
105    }
106
107    /// Transitions to the next phase if current phase is approved
108    ///
109    /// Enforces sequential phase progression. Only allows transition to the next
110    /// phase if the current phase has been explicitly approved.
111    ///
112    /// # Arguments
113    ///
114    /// * `session` - The spec writing session
115    ///
116    /// # Returns
117    ///
118    /// Updated session with new phase, or error if transition is not allowed
119    pub fn transition_to_next_phase(session: &mut SpecWritingSession) -> Result<(), SpecError> {
120        // Check if current phase is approved
121        let current_gate = session
122            .approval_gates
123            .iter()
124            .find(|g| g.phase == session.phase)
125            .ok_or_else(|| {
126                SpecError::InvalidFormat(format!(
127                    "No approval gate found for phase {:?}",
128                    session.phase
129                ))
130            })?;
131
132        if !current_gate.approved {
133            return Err(SpecError::InvalidFormat(format!(
134                "Cannot transition from {:?}: phase not approved",
135                session.phase
136            )));
137        }
138
139        // Determine next phase
140        let next_phase = match session.phase {
141            SpecPhase::Discovery => SpecPhase::Requirements,
142            SpecPhase::Requirements => SpecPhase::Design,
143            SpecPhase::Design => SpecPhase::Tasks,
144            SpecPhase::Tasks => SpecPhase::Execution,
145            SpecPhase::Execution => {
146                return Err(SpecError::InvalidFormat(
147                    "Already at final phase (Execution)".to_string(),
148                ))
149            }
150        };
151
152        // Verify next phase gate exists
153        if !session.approval_gates.iter().any(|g| g.phase == next_phase) {
154            return Err(SpecError::InvalidFormat(format!(
155                "No approval gate found for next phase {:?}",
156                next_phase
157            )));
158        }
159
160        // Transition to next phase
161        session.phase = next_phase;
162        session.updated_at = Utc::now();
163
164        Ok(())
165    }
166
167    /// Checks if a phase transition is allowed
168    ///
169    /// Returns true if the current phase is approved and the next phase exists.
170    pub fn can_transition(session: &SpecWritingSession) -> bool {
171        // Check if current phase is approved
172        let current_gate = session
173            .approval_gates
174            .iter()
175            .find(|g| g.phase == session.phase);
176
177        if let Some(gate) = current_gate {
178            if !gate.approved {
179                return false;
180            }
181        } else {
182            return false;
183        }
184
185        // Check if we're not at the final phase
186        !matches!(session.phase, SpecPhase::Execution)
187    }
188
189    /// Gets the approval status for a specific phase
190    ///
191    /// Returns the approval gate for the given phase, or None if not found.
192    pub fn get_phase_approval(
193        session: &SpecWritingSession,
194        phase: SpecPhase,
195    ) -> Option<&ApprovalGate> {
196        session.approval_gates.iter().find(|g| g.phase == phase)
197    }
198
199    /// Gets all approval gates for a session
200    pub fn get_all_approvals(session: &SpecWritingSession) -> &[ApprovalGate] {
201        &session.approval_gates
202    }
203
204    /// Checks if all phases up to and including the given phase are approved
205    ///
206    /// Useful for validating that a session has completed all required phases.
207    pub fn are_phases_approved_up_to(
208        session: &SpecWritingSession,
209        target_phase: SpecPhase,
210    ) -> bool {
211        let phases = vec![
212            SpecPhase::Discovery,
213            SpecPhase::Requirements,
214            SpecPhase::Design,
215            SpecPhase::Tasks,
216            SpecPhase::Execution,
217        ];
218
219        for phase in phases {
220            if let Some(gate) = session.approval_gates.iter().find(|g| g.phase == phase) {
221                if !gate.approved {
222                    return false;
223                }
224            }
225
226            if phase == target_phase {
227                break;
228            }
229        }
230
231        true
232    }
233}
234
235impl Default for ApprovalManager {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use chrono::Utc;
245
246    fn create_test_session() -> SpecWritingSession {
247        let now = Utc::now();
248        SpecWritingSession {
249            id: "test-session".to_string(),
250            spec_id: "test-spec".to_string(),
251            phase: SpecPhase::Discovery,
252            conversation_history: vec![],
253            approval_gates: ApprovalManager::initialize_gates(),
254            created_at: now,
255            updated_at: now,
256        }
257    }
258
259    #[test]
260    fn test_initialize_gates() {
261        let gates = ApprovalManager::initialize_gates();
262
263        assert_eq!(gates.len(), 5);
264        assert_eq!(gates[0].phase, SpecPhase::Discovery);
265        assert_eq!(gates[1].phase, SpecPhase::Requirements);
266        assert_eq!(gates[2].phase, SpecPhase::Design);
267        assert_eq!(gates[3].phase, SpecPhase::Tasks);
268        assert_eq!(gates[4].phase, SpecPhase::Execution);
269
270        for gate in gates {
271            assert!(!gate.approved);
272            assert!(gate.approved_at.is_none());
273            assert!(gate.approved_by.is_none());
274            assert!(gate.feedback.is_none());
275        }
276    }
277
278    #[test]
279    fn test_approve_phase() {
280        let mut session = create_test_session();
281
282        let result = ApprovalManager::approve_phase(
283            &mut session,
284            "reviewer",
285            Some("Looks good".to_string()),
286        );
287        assert!(result.is_ok());
288
289        let gate = ApprovalManager::get_phase_approval(&session, SpecPhase::Discovery).unwrap();
290        assert!(gate.approved);
291        assert_eq!(gate.approved_by, Some("reviewer".to_string()));
292        assert_eq!(gate.feedback, Some("Looks good".to_string()));
293        assert!(gate.approved_at.is_some());
294    }
295
296    #[test]
297    fn test_approve_phase_without_feedback() {
298        let mut session = create_test_session();
299
300        let result = ApprovalManager::approve_phase(&mut session, "reviewer", None);
301        assert!(result.is_ok());
302
303        let gate = ApprovalManager::get_phase_approval(&session, SpecPhase::Discovery).unwrap();
304        assert!(gate.approved);
305        assert!(gate.feedback.is_none());
306    }
307
308    #[test]
309    fn test_cannot_transition_without_approval() {
310        let session = create_test_session();
311
312        assert!(!ApprovalManager::can_transition(&session));
313    }
314
315    #[test]
316    fn test_transition_to_next_phase_fails_without_approval() {
317        let mut session = create_test_session();
318
319        let result = ApprovalManager::transition_to_next_phase(&mut session);
320        assert!(result.is_err());
321        assert_eq!(session.phase, SpecPhase::Discovery);
322    }
323
324    #[test]
325    fn test_transition_to_next_phase_succeeds_with_approval() {
326        let mut session = create_test_session();
327
328        // Approve current phase
329        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
330
331        // Transition should succeed
332        let result = ApprovalManager::transition_to_next_phase(&mut session);
333        assert!(result.is_ok());
334        assert_eq!(session.phase, SpecPhase::Requirements);
335    }
336
337    #[test]
338    fn test_sequential_phase_progression() {
339        let mut session = create_test_session();
340
341        let phases = vec![
342            SpecPhase::Discovery,
343            SpecPhase::Requirements,
344            SpecPhase::Design,
345            SpecPhase::Tasks,
346            SpecPhase::Execution,
347        ];
348
349        for (i, phase) in phases.iter().enumerate() {
350            assert_eq!(session.phase, *phase);
351
352            // Approve current phase
353            ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
354
355            // Transition to next phase (except for last phase)
356            if i < phases.len() - 1 {
357                ApprovalManager::transition_to_next_phase(&mut session).unwrap();
358            }
359        }
360
361        assert_eq!(session.phase, SpecPhase::Execution);
362    }
363
364    #[test]
365    fn test_cannot_transition_from_execution() {
366        let mut session = create_test_session();
367
368        // Manually set to execution phase
369        session.phase = SpecPhase::Execution;
370
371        // Approve execution phase
372        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
373
374        // Try to transition from execution
375        let result = ApprovalManager::transition_to_next_phase(&mut session);
376        assert!(result.is_err());
377    }
378
379    #[test]
380    fn test_get_phase_approval() {
381        let session = create_test_session();
382
383        let gate = ApprovalManager::get_phase_approval(&session, SpecPhase::Requirements);
384        assert!(gate.is_some());
385        assert_eq!(gate.unwrap().phase, SpecPhase::Requirements);
386        assert!(!gate.unwrap().approved);
387    }
388
389    #[test]
390    fn test_get_all_approvals() {
391        let session = create_test_session();
392
393        let gates = ApprovalManager::get_all_approvals(&session);
394        assert_eq!(gates.len(), 5);
395    }
396
397    #[test]
398    fn test_are_phases_approved_up_to() {
399        let mut session = create_test_session();
400
401        // Initially, no phases are approved
402        assert!(!ApprovalManager::are_phases_approved_up_to(
403            &session,
404            SpecPhase::Requirements
405        ));
406
407        // Approve discovery
408        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
409        ApprovalManager::transition_to_next_phase(&mut session).unwrap();
410
411        // Approve requirements
412        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
413
414        // Check that phases up to requirements are approved
415        assert!(ApprovalManager::are_phases_approved_up_to(
416            &session,
417            SpecPhase::Requirements
418        ));
419
420        // Check that phases up to design are not approved
421        assert!(!ApprovalManager::are_phases_approved_up_to(
422            &session,
423            SpecPhase::Design
424        ));
425    }
426
427    #[test]
428    fn test_can_transition_after_approval() {
429        let mut session = create_test_session();
430
431        assert!(!ApprovalManager::can_transition(&session));
432
433        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
434
435        assert!(ApprovalManager::can_transition(&session));
436    }
437
438    #[test]
439    fn test_cannot_transition_from_final_phase() {
440        let mut session = create_test_session();
441
442        session.phase = SpecPhase::Execution;
443        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
444
445        assert!(!ApprovalManager::can_transition(&session));
446    }
447
448    #[test]
449    fn test_approval_timestamps_are_recorded() {
450        let mut session = create_test_session();
451        let before = Utc::now();
452
453        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
454
455        let after = Utc::now();
456        let gate = ApprovalManager::get_phase_approval(&session, SpecPhase::Discovery).unwrap();
457
458        assert!(gate.approved_at.is_some());
459        let approved_at = gate.approved_at.unwrap();
460        assert!(approved_at >= before);
461        assert!(approved_at <= after);
462    }
463
464    #[test]
465    fn test_session_updated_at_changes_on_approval() {
466        let mut session = create_test_session();
467        let original_updated_at = session.updated_at;
468
469        // Small delay to ensure timestamp difference
470        std::thread::sleep(std::time::Duration::from_millis(10));
471
472        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
473
474        assert!(session.updated_at > original_updated_at);
475    }
476
477    #[test]
478    fn test_session_updated_at_changes_on_transition() {
479        let mut session = create_test_session();
480
481        ApprovalManager::approve_phase(&mut session, "reviewer", None).unwrap();
482
483        let updated_at_after_approval = session.updated_at;
484
485        // Small delay to ensure timestamp difference
486        std::thread::sleep(std::time::Duration::from_millis(10));
487
488        ApprovalManager::transition_to_next_phase(&mut session).unwrap();
489
490        assert!(session.updated_at > updated_at_after_approval);
491    }
492}