ricecoder_specs/
ai_writer.rs

1//! AI-assisted spec writing with phase guidance and approval gates
2//!
3//! Manages AI-assisted spec creation through sequential phases (requirements → design → tasks)
4//! with conversation history, gap identification, and integration with approval gates and validation.
5
6use crate::approval::ApprovalManager;
7use crate::error::SpecError;
8use crate::models::{
9    ConversationMessage, MessageRole, Spec, SpecMetadata, SpecPhase, SpecStatus,
10    SpecWritingSession, Steering,
11};
12use crate::steering::SteeringLoader;
13use crate::validation::ValidationEngine;
14use chrono::Utc;
15
16/// Manages AI-assisted spec writing with phase guidance and approval gates
17#[derive(Debug, Clone)]
18pub struct AISpecWriter;
19
20/// Gap analysis result for a spec
21#[derive(Debug, Clone)]
22pub struct GapAnalysis {
23    /// Missing sections in the spec
24    pub missing_sections: Vec<String>,
25    /// Incomplete sections
26    pub incomplete_sections: Vec<String>,
27    /// Suggestions for improvement
28    pub suggestions: Vec<String>,
29}
30
31impl AISpecWriter {
32    /// Creates a new AI spec writer
33    pub fn new() -> Self {
34        AISpecWriter
35    }
36
37    /// Initializes a new spec writing session
38    ///
39    /// Creates a new session with initial spec and approval gates.
40    /// Session starts in Discovery phase.
41    ///
42    /// # Arguments
43    ///
44    /// * `spec_id` - Unique identifier for the spec
45    /// * `spec_name` - Human-readable name for the spec
46    ///
47    /// # Returns
48    ///
49    /// New spec writing session ready for phase guidance
50    pub fn initialize_session(spec_id: &str, spec_name: &str) -> SpecWritingSession {
51        let now = Utc::now();
52        let spec = Spec {
53            id: spec_id.to_string(),
54            name: spec_name.to_string(),
55            version: "0.1.0".to_string(),
56            requirements: vec![],
57            design: None,
58            tasks: vec![],
59            metadata: SpecMetadata {
60                author: None,
61                created_at: now,
62                updated_at: now,
63                phase: SpecPhase::Discovery,
64                status: SpecStatus::Draft,
65            },
66            inheritance: None,
67        };
68
69        SpecWritingSession {
70            id: format!("session-{}", uuid::Uuid::new_v4()),
71            spec_id: spec.id.clone(),
72            phase: SpecPhase::Discovery,
73            conversation_history: vec![],
74            approval_gates: ApprovalManager::initialize_gates(),
75            created_at: now,
76            updated_at: now,
77        }
78    }
79
80    /// Adds a message to the conversation history
81    ///
82    /// Records user or assistant messages in the session for context.
83    ///
84    /// # Arguments
85    ///
86    /// * `session` - The spec writing session
87    /// * `role` - Role of the message sender (User, Assistant, System)
88    /// * `content` - Message content
89    ///
90    /// # Returns
91    ///
92    /// Updated session with message added
93    pub fn add_message(
94        session: &mut SpecWritingSession,
95        role: MessageRole,
96        content: &str,
97    ) -> Result<(), SpecError> {
98        let message = ConversationMessage {
99            id: format!("msg-{}", uuid::Uuid::new_v4()),
100            spec_id: session.spec_id.clone(),
101            role,
102            content: content.to_string(),
103            timestamp: Utc::now(),
104        };
105
106        session.conversation_history.push(message);
107        session.updated_at = Utc::now();
108
109        Ok(())
110    }
111
112    /// Gets phase-specific guidance for the current phase
113    ///
114    /// Provides guidance on what to focus on in the current phase.
115    ///
116    /// # Arguments
117    ///
118    /// * `session` - The spec writing session
119    ///
120    /// # Returns
121    ///
122    /// Guidance text for the current phase
123    pub fn get_phase_guidance(session: &SpecWritingSession) -> String {
124        match session.phase {
125            SpecPhase::Discovery => {
126                "Discovery Phase: Research the problem space, identify user personas, define scope, and establish feature hierarchy. Focus on understanding the problem before designing solutions.".to_string()
127            }
128            SpecPhase::Requirements => {
129                "Requirements Phase: Define what must be built using user stories and acceptance criteria. Write EARS-compliant requirements with clear success conditions.".to_string()
130            }
131            SpecPhase::Design => {
132                "Design Phase: Create technical design and architecture to satisfy requirements. Define data models, algorithms, and integration points.".to_string()
133            }
134            SpecPhase::Tasks => {
135                "Tasks Phase: Break design into hierarchical implementation tasks. Define dependencies, ordering, and exit criteria for each task.".to_string()
136            }
137            SpecPhase::Execution => {
138                "Execution Phase: implement according to spec and validate against acceptance criteria. Track task completion and validate requirements are met.".to_string()
139            }
140        }
141    }
142
143    /// Analyzes gaps in the current spec
144    ///
145    /// Identifies missing sections, incomplete sections, and suggests improvements
146    /// based on the current phase.
147    ///
148    /// # Arguments
149    ///
150    /// * `session` - The spec writing session
151    /// * `spec` - The spec being written
152    ///
153    /// # Returns
154    ///
155    /// Gap analysis with missing sections and suggestions
156    pub fn analyze_gaps(session: &SpecWritingSession, spec: &Spec) -> GapAnalysis {
157        let mut missing_sections = Vec::new();
158        let mut incomplete_sections = Vec::new();
159        let mut suggestions = Vec::new();
160
161        match session.phase {
162            SpecPhase::Discovery => {
163                if spec.requirements.is_empty() {
164                    suggestions.push(
165                        "Consider researching similar products and existing solutions".to_string(),
166                    );
167                }
168                suggestions.push("Define feature hierarchy (MVP → Phase 2 → Phase 3)".to_string());
169                suggestions.push("Document constraints and dependencies".to_string());
170            }
171            SpecPhase::Requirements => {
172                if spec.requirements.is_empty() {
173                    missing_sections.push("Requirements".to_string());
174                    suggestions.push(
175                        "Add at least one requirement with user story and acceptance criteria"
176                            .to_string(),
177                    );
178                }
179
180                for req in &spec.requirements {
181                    if req.user_story.is_empty() {
182                        incomplete_sections
183                            .push(format!("Requirement {}: missing user story", req.id));
184                    }
185                    if req.acceptance_criteria.is_empty() {
186                        incomplete_sections.push(format!(
187                            "Requirement {}: missing acceptance criteria",
188                            req.id
189                        ));
190                    }
191                }
192
193                suggestions
194                    .push("Ensure each requirement has clear acceptance criteria".to_string());
195                suggestions.push("Prioritize requirements (MUST/SHOULD/COULD)".to_string());
196            }
197            SpecPhase::Design => {
198                if spec.design.is_none() {
199                    missing_sections.push("Design".to_string());
200                    suggestions
201                        .push("Create design document with overview and architecture".to_string());
202                } else if let Some(design) = &spec.design {
203                    if design.overview.is_empty() {
204                        incomplete_sections.push("Design: missing overview".to_string());
205                    }
206                    if design.architecture.is_empty() {
207                        incomplete_sections.push("Design: missing architecture".to_string());
208                    }
209                    if design.components.is_empty() {
210                        incomplete_sections.push("Design: missing components".to_string());
211                    }
212                    if design.correctness_properties.is_empty() {
213                        incomplete_sections
214                            .push("Design: missing correctness properties".to_string());
215                    }
216                }
217
218                suggestions.push("Define data models and type definitions".to_string());
219                suggestions.push("Specify algorithms and key logic".to_string());
220                suggestions.push("Document integration points and dependencies".to_string());
221            }
222            SpecPhase::Tasks => {
223                if spec.tasks.is_empty() {
224                    missing_sections.push("Tasks".to_string());
225                    suggestions.push("Break design into concrete, implementable tasks".to_string());
226                }
227
228                for task in &spec.tasks {
229                    if task.description.is_empty() {
230                        incomplete_sections.push(format!("Task {}: missing description", task.id));
231                    }
232                    if task.requirements.is_empty() {
233                        incomplete_sections
234                            .push(format!("Task {}: missing requirement links", task.id));
235                    }
236                }
237
238                suggestions.push("Define clear dependencies between tasks".to_string());
239                suggestions.push("Specify exit criteria for each task".to_string());
240            }
241            SpecPhase::Execution => {
242                suggestions.push("Implement tasks in dependency order".to_string());
243                suggestions.push("Validate each task against exit criteria".to_string());
244                suggestions.push("Run acceptance tests against requirements".to_string());
245            }
246        }
247
248        GapAnalysis {
249            missing_sections,
250            incomplete_sections,
251            suggestions,
252        }
253    }
254
255    /// Validates spec against EARS and INCOSE rules
256    ///
257    /// Runs validation engine to check EARS compliance and INCOSE semantic rules.
258    ///
259    /// # Arguments
260    ///
261    /// * `spec` - The spec to validate
262    ///
263    /// # Returns
264    ///
265    /// Ok if validation passes, Err with validation errors if it fails
266    pub fn validate_spec(spec: &Spec) -> Result<(), SpecError> {
267        ValidationEngine::validate(spec)
268    }
269
270    /// Requests approval for the current phase
271    ///
272    /// Records approval with timestamp and approver information.
273    /// Prevents phase transitions without explicit approval.
274    ///
275    /// # Arguments
276    ///
277    /// * `session` - The spec writing session
278    /// * `approver` - Name of the person approving
279    /// * `feedback` - Optional feedback on the phase
280    ///
281    /// # Returns
282    ///
283    /// Ok if approval is recorded, Err if approval fails
284    pub fn request_approval(
285        session: &mut SpecWritingSession,
286        approver: &str,
287        feedback: Option<String>,
288    ) -> Result<(), SpecError> {
289        ApprovalManager::approve_phase(session, approver, feedback)
290    }
291
292    /// Transitions to the next phase
293    ///
294    /// Enforces sequential phase progression. Only allows transition if current phase
295    /// has been explicitly approved.
296    ///
297    /// # Arguments
298    ///
299    /// * `session` - The spec writing session
300    ///
301    /// # Returns
302    ///
303    /// Ok if transition succeeds, Err if transition is not allowed
304    pub fn transition_to_next_phase(session: &mut SpecWritingSession) -> Result<(), SpecError> {
305        ApprovalManager::transition_to_next_phase(session)
306    }
307
308    /// Checks if phase transition is allowed
309    ///
310    /// Returns true if current phase is approved and next phase exists.
311    pub fn can_transition(session: &SpecWritingSession) -> bool {
312        ApprovalManager::can_transition(session)
313    }
314
315    /// Gets the conversation history for context
316    ///
317    /// Returns all messages in the session for AI context.
318    pub fn get_conversation_history(session: &SpecWritingSession) -> &[ConversationMessage] {
319        &session.conversation_history
320    }
321
322    /// Gets the current phase
323    pub fn get_current_phase(session: &SpecWritingSession) -> SpecPhase {
324        session.phase
325    }
326
327    /// Checks if all phases up to a target phase are approved
328    ///
329    /// Useful for validating that a session has completed required phases.
330    pub fn are_phases_approved_up_to(
331        session: &SpecWritingSession,
332        target_phase: SpecPhase,
333    ) -> bool {
334        ApprovalManager::are_phases_approved_up_to(session, target_phase)
335    }
336
337    /// Builds an AI prompt with steering context included
338    ///
339    /// Generates a prompt for AI spec writing that includes steering rules,
340    /// standards, and templates. Project steering takes precedence over global steering.
341    ///
342    /// # Arguments
343    ///
344    /// * `session` - The spec writing session
345    /// * `global_steering` - Global steering document (workspace-level)
346    /// * `project_steering` - Project steering document (project-level)
347    /// * `user_input` - The user's input or question
348    ///
349    /// # Returns
350    ///
351    /// A formatted prompt string with steering context included
352    pub fn build_prompt_with_steering_context(
353        session: &SpecWritingSession,
354        global_steering: &Steering,
355        project_steering: &Steering,
356        user_input: &str,
357    ) -> Result<String, SpecError> {
358        // Merge steering with project taking precedence
359        let merged_steering = SteeringLoader::merge(global_steering, project_steering)?;
360
361        // Build the prompt with steering context
362        let mut prompt = String::new();
363
364        // Add phase guidance
365        let phase_guidance = Self::get_phase_guidance(session);
366        prompt.push_str(&format!("## Phase Guidance\n{}\n\n", phase_guidance));
367
368        // Add steering rules if any
369        if !merged_steering.rules.is_empty() {
370            prompt.push_str("## Steering Rules\n");
371            for rule in &merged_steering.rules {
372                prompt.push_str(&format!(
373                    "- **{}**: {} (Pattern: {}, Action: {})\n",
374                    rule.id, rule.description, rule.pattern, rule.action
375                ));
376            }
377            prompt.push('\n');
378        }
379
380        // Add standards if any
381        if !merged_steering.standards.is_empty() {
382            prompt.push_str("## Standards\n");
383            for standard in &merged_steering.standards {
384                prompt.push_str(&format!(
385                    "- **{}**: {}\n",
386                    standard.id, standard.description
387                ));
388            }
389            prompt.push('\n');
390        }
391
392        // Add templates if any
393        if !merged_steering.templates.is_empty() {
394            prompt.push_str("## Available Templates\n");
395            for template in &merged_steering.templates {
396                prompt.push_str(&format!("- **{}**: {}\n", template.id, template.path));
397            }
398            prompt.push('\n');
399        }
400
401        // Add conversation history for context
402        if !session.conversation_history.is_empty() {
403            prompt.push_str("## Conversation History\n");
404            for msg in &session.conversation_history {
405                let role_str = match msg.role {
406                    MessageRole::User => "User",
407                    MessageRole::Assistant => "Assistant",
408                    MessageRole::System => "System",
409                };
410                prompt.push_str(&format!("**{}**: {}\n", role_str, msg.content));
411            }
412            prompt.push('\n');
413        }
414
415        // Add the user's current input
416        prompt.push_str(&format!("## Current Request\n{}\n", user_input));
417
418        Ok(prompt)
419    }
420
421    /// Applies steering rules to generated specs
422    ///
423    /// Validates that generated specs conform to steering standards.
424    /// Returns a list of violations if any steering rules are not followed.
425    ///
426    /// # Arguments
427    ///
428    /// * `spec` - The generated spec to validate
429    /// * `steering` - The steering document with rules and standards
430    ///
431    /// # Returns
432    ///
433    /// Ok with list of violations (empty if all rules are followed),
434    /// or Err if validation fails
435    pub fn validate_spec_against_steering(
436        spec: &Spec,
437        steering: &Steering,
438    ) -> Result<Vec<String>, SpecError> {
439        let mut violations = Vec::new();
440
441        // Check that spec has required metadata
442        if spec.metadata.author.is_none() {
443            violations.push("Spec missing author in metadata".to_string());
444        }
445
446        // Check that spec has a version
447        if spec.version.is_empty() {
448            violations.push("Spec missing version".to_string());
449        }
450
451        // Check that spec name follows naming standards
452        // (This is a simple check; more sophisticated checks could be added)
453        if spec.name.is_empty() {
454            violations.push("Spec missing name".to_string());
455        }
456
457        // Check requirements phase has requirements
458        if spec.metadata.phase == SpecPhase::Requirements && spec.requirements.is_empty() {
459            violations.push("Requirements phase spec has no requirements".to_string());
460        }
461
462        // Check design phase has design
463        if spec.metadata.phase == SpecPhase::Design && spec.design.is_none() {
464            violations.push("Design phase spec has no design".to_string());
465        }
466
467        // Check tasks phase has tasks
468        if spec.metadata.phase == SpecPhase::Tasks && spec.tasks.is_empty() {
469            violations.push("Tasks phase spec has no tasks".to_string());
470        }
471
472        // Validate against steering standards
473        for standard in &steering.standards {
474            // Check that all requirements have acceptance criteria (common standard)
475            if standard.id == "require-acceptance-criteria" {
476                for req in &spec.requirements {
477                    if req.acceptance_criteria.is_empty() {
478                        violations.push(format!(
479                            "Requirement {} violates standard {}: missing acceptance criteria",
480                            req.id, standard.id
481                        ));
482                    }
483                }
484            }
485
486            // Check that all tasks have descriptions (common standard)
487            if standard.id == "require-task-descriptions" {
488                for task in &spec.tasks {
489                    if task.description.is_empty() {
490                        violations.push(format!(
491                            "Task {} violates standard {}: missing description",
492                            task.id, standard.id
493                        ));
494                    }
495                }
496            }
497        }
498
499        Ok(violations)
500    }
501
502    /// Gets steering context as a formatted string for display
503    ///
504    /// Useful for showing users what steering rules are active.
505    ///
506    /// # Arguments
507    ///
508    /// * `steering` - The steering document
509    ///
510    /// # Returns
511    ///
512    /// Formatted string representation of steering context
513    pub fn format_steering_context(steering: &Steering) -> String {
514        let mut output = String::new();
515
516        if !steering.rules.is_empty() {
517            output.push_str("### Steering Rules\n");
518            for rule in &steering.rules {
519                output.push_str(&format!(
520                    "- `{}`: {} ({})\n",
521                    rule.id, rule.description, rule.action
522                ));
523            }
524            output.push('\n');
525        }
526
527        if !steering.standards.is_empty() {
528            output.push_str("### Standards\n");
529            for standard in &steering.standards {
530                output.push_str(&format!("- `{}`: {}\n", standard.id, standard.description));
531            }
532            output.push('\n');
533        }
534
535        if !steering.templates.is_empty() {
536            output.push_str("### Templates\n");
537            for template in &steering.templates {
538                output.push_str(&format!("- `{}`: {}\n", template.id, template.path));
539            }
540        }
541
542        output
543    }
544}
545
546impl Default for AISpecWriter {
547    fn default() -> Self {
548        Self::new()
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use crate::models::{AcceptanceCriterion, Design, Priority, Requirement, Task};
556
557    fn create_test_session() -> SpecWritingSession {
558        AISpecWriter::initialize_session("test-spec", "Test Spec")
559    }
560
561    fn create_test_spec() -> Spec {
562        let now = Utc::now();
563        Spec {
564            id: "test-spec".to_string(),
565            name: "Test Spec".to_string(),
566            version: "0.1.0".to_string(),
567            requirements: vec![],
568            design: None,
569            tasks: vec![],
570            metadata: SpecMetadata {
571                author: None,
572                created_at: now,
573                updated_at: now,
574                phase: SpecPhase::Discovery,
575                status: SpecStatus::Draft,
576            },
577            inheritance: None,
578        }
579    }
580
581    #[test]
582    fn test_initialize_session() {
583        let session = AISpecWriter::initialize_session("test-spec", "Test Spec");
584
585        assert_eq!(session.spec_id, "test-spec");
586        assert_eq!(session.phase, SpecPhase::Discovery);
587        assert!(session.conversation_history.is_empty());
588        assert_eq!(session.approval_gates.len(), 5);
589    }
590
591    #[test]
592    fn test_add_message_user() {
593        let mut session = create_test_session();
594
595        let result =
596            AISpecWriter::add_message(&mut session, MessageRole::User, "What should we build?");
597        assert!(result.is_ok());
598        assert_eq!(session.conversation_history.len(), 1);
599        assert_eq!(session.conversation_history[0].role, MessageRole::User);
600        assert_eq!(
601            session.conversation_history[0].content,
602            "What should we build?"
603        );
604    }
605
606    #[test]
607    fn test_add_message_assistant() {
608        let mut session = create_test_session();
609
610        AISpecWriter::add_message(&mut session, MessageRole::User, "What should we build?")
611            .unwrap();
612        AISpecWriter::add_message(
613            &mut session,
614            MessageRole::Assistant,
615            "Let's build a task manager",
616        )
617        .unwrap();
618
619        assert_eq!(session.conversation_history.len(), 2);
620        assert_eq!(session.conversation_history[1].role, MessageRole::Assistant);
621    }
622
623    #[test]
624    fn test_add_multiple_messages() {
625        let mut session = create_test_session();
626
627        for i in 0..5 {
628            AISpecWriter::add_message(&mut session, MessageRole::User, &format!("Message {}", i))
629                .unwrap();
630        }
631
632        assert_eq!(session.conversation_history.len(), 5);
633    }
634
635    #[test]
636    fn test_get_phase_guidance_discovery() {
637        let session = create_test_session();
638        let guidance = AISpecWriter::get_phase_guidance(&session);
639
640        assert!(guidance.contains("Discovery"));
641        assert!(guidance.contains("problem space"));
642    }
643
644    #[test]
645    fn test_get_phase_guidance_requirements() {
646        let mut session = create_test_session();
647        session.phase = SpecPhase::Requirements;
648
649        let guidance = AISpecWriter::get_phase_guidance(&session);
650
651        assert!(guidance.contains("Requirements"));
652        assert!(guidance.contains("user stories"));
653    }
654
655    #[test]
656    fn test_get_phase_guidance_design() {
657        let mut session = create_test_session();
658        session.phase = SpecPhase::Design;
659
660        let guidance = AISpecWriter::get_phase_guidance(&session);
661
662        assert!(guidance.contains("Design"));
663        assert!(guidance.contains("architecture"));
664    }
665
666    #[test]
667    fn test_get_phase_guidance_tasks() {
668        let mut session = create_test_session();
669        session.phase = SpecPhase::Tasks;
670
671        let guidance = AISpecWriter::get_phase_guidance(&session);
672
673        assert!(guidance.contains("Tasks"));
674        assert!(guidance.contains("implementation"));
675    }
676
677    #[test]
678    fn test_get_phase_guidance_execution() {
679        let mut session = create_test_session();
680        session.phase = SpecPhase::Execution;
681
682        let guidance = AISpecWriter::get_phase_guidance(&session);
683
684        assert!(guidance.contains("Execution"));
685        assert!(guidance.contains("implement"));
686    }
687
688    #[test]
689    fn test_analyze_gaps_discovery_empty() {
690        let session = create_test_session();
691        let spec = create_test_spec();
692
693        let gaps = AISpecWriter::analyze_gaps(&session, &spec);
694
695        assert!(!gaps.suggestions.is_empty());
696        assert!(gaps.suggestions.iter().any(|s| s.contains("research")));
697    }
698
699    #[test]
700    fn test_analyze_gaps_requirements_missing() {
701        let mut session = create_test_session();
702        session.phase = SpecPhase::Requirements;
703        let spec = create_test_spec();
704
705        let gaps = AISpecWriter::analyze_gaps(&session, &spec);
706
707        assert!(gaps.missing_sections.contains(&"Requirements".to_string()));
708    }
709
710    #[test]
711    fn test_analyze_gaps_requirements_incomplete() {
712        let mut session = create_test_session();
713        session.phase = SpecPhase::Requirements;
714
715        let mut spec = create_test_spec();
716        spec.requirements.push(Requirement {
717            id: "REQ-1".to_string(),
718            user_story: "".to_string(),
719            acceptance_criteria: vec![],
720            priority: Priority::Must,
721        });
722
723        let gaps = AISpecWriter::analyze_gaps(&session, &spec);
724
725        assert!(!gaps.incomplete_sections.is_empty());
726    }
727
728    #[test]
729    fn test_analyze_gaps_design_missing() {
730        let mut session = create_test_session();
731        session.phase = SpecPhase::Design;
732        let spec = create_test_spec();
733
734        let gaps = AISpecWriter::analyze_gaps(&session, &spec);
735
736        assert!(gaps.missing_sections.contains(&"Design".to_string()));
737    }
738
739    #[test]
740    fn test_analyze_gaps_tasks_missing() {
741        let mut session = create_test_session();
742        session.phase = SpecPhase::Tasks;
743        let spec = create_test_spec();
744
745        let gaps = AISpecWriter::analyze_gaps(&session, &spec);
746
747        assert!(gaps.missing_sections.contains(&"Tasks".to_string()));
748    }
749
750    #[test]
751    fn test_request_approval() {
752        let mut session = create_test_session();
753
754        let result = AISpecWriter::request_approval(
755            &mut session,
756            "reviewer",
757            Some("Looks good".to_string()),
758        );
759        assert!(result.is_ok());
760
761        let gate = session
762            .approval_gates
763            .iter()
764            .find(|g| g.phase == SpecPhase::Discovery)
765            .unwrap();
766        assert!(gate.approved);
767    }
768
769    #[test]
770    fn test_can_transition_before_approval() {
771        let session = create_test_session();
772
773        assert!(!AISpecWriter::can_transition(&session));
774    }
775
776    #[test]
777    fn test_can_transition_after_approval() {
778        let mut session = create_test_session();
779
780        AISpecWriter::request_approval(&mut session, "reviewer", None).unwrap();
781
782        assert!(AISpecWriter::can_transition(&session));
783    }
784
785    #[test]
786    fn test_transition_to_next_phase() {
787        let mut session = create_test_session();
788
789        AISpecWriter::request_approval(&mut session, "reviewer", None).unwrap();
790        let result = AISpecWriter::transition_to_next_phase(&mut session);
791
792        assert!(result.is_ok());
793        assert_eq!(session.phase, SpecPhase::Requirements);
794    }
795
796    #[test]
797    fn test_transition_fails_without_approval() {
798        let mut session = create_test_session();
799
800        let result = AISpecWriter::transition_to_next_phase(&mut session);
801
802        assert!(result.is_err());
803        assert_eq!(session.phase, SpecPhase::Discovery);
804    }
805
806    #[test]
807    fn test_get_conversation_history() {
808        let mut session = create_test_session();
809
810        AISpecWriter::add_message(&mut session, MessageRole::User, "Message 1").unwrap();
811        AISpecWriter::add_message(&mut session, MessageRole::Assistant, "Message 2").unwrap();
812
813        let history = AISpecWriter::get_conversation_history(&session);
814
815        assert_eq!(history.len(), 2);
816        assert_eq!(history[0].role, MessageRole::User);
817        assert_eq!(history[1].role, MessageRole::Assistant);
818    }
819
820    #[test]
821    fn test_get_current_phase() {
822        let mut session = create_test_session();
823
824        assert_eq!(
825            AISpecWriter::get_current_phase(&session),
826            SpecPhase::Discovery
827        );
828
829        session.phase = SpecPhase::Requirements;
830
831        assert_eq!(
832            AISpecWriter::get_current_phase(&session),
833            SpecPhase::Requirements
834        );
835    }
836
837    #[test]
838    fn test_are_phases_approved_up_to() {
839        let mut session = create_test_session();
840
841        assert!(!AISpecWriter::are_phases_approved_up_to(
842            &session,
843            SpecPhase::Requirements
844        ));
845
846        AISpecWriter::request_approval(&mut session, "reviewer", None).unwrap();
847        AISpecWriter::transition_to_next_phase(&mut session).unwrap();
848        AISpecWriter::request_approval(&mut session, "reviewer", None).unwrap();
849
850        assert!(AISpecWriter::are_phases_approved_up_to(
851            &session,
852            SpecPhase::Requirements
853        ));
854    }
855
856    #[test]
857    fn test_sequential_phase_workflow() {
858        let mut session = create_test_session();
859        let mut spec = create_test_spec();
860
861        // Discovery phase
862        assert_eq!(session.phase, SpecPhase::Discovery);
863        let guidance = AISpecWriter::get_phase_guidance(&session);
864        assert!(guidance.contains("Discovery"));
865
866        AISpecWriter::add_message(
867            &mut session,
868            MessageRole::User,
869            "Let's build a task manager",
870        )
871        .unwrap();
872        AISpecWriter::request_approval(&mut session, "reviewer", None).unwrap();
873        AISpecWriter::transition_to_next_phase(&mut session).unwrap();
874
875        // Requirements phase
876        assert_eq!(session.phase, SpecPhase::Requirements);
877        spec.requirements.push(Requirement {
878            id: "REQ-1".to_string(),
879            user_story: "As a user, I want to create tasks".to_string(),
880            acceptance_criteria: vec![AcceptanceCriterion {
881                id: "AC-1.1".to_string(),
882                when: "user enters task".to_string(),
883                then: "task is added".to_string(),
884            }],
885            priority: Priority::Must,
886        });
887
888        AISpecWriter::request_approval(&mut session, "reviewer", None).unwrap();
889        AISpecWriter::transition_to_next_phase(&mut session).unwrap();
890
891        // Design phase
892        assert_eq!(session.phase, SpecPhase::Design);
893        spec.design = Some(Design {
894            overview: "Task management system".to_string(),
895            architecture: "Layered architecture".to_string(),
896            components: vec![],
897            data_models: vec![],
898            correctness_properties: vec![],
899        });
900
901        AISpecWriter::request_approval(&mut session, "reviewer", None).unwrap();
902        AISpecWriter::transition_to_next_phase(&mut session).unwrap();
903
904        // Tasks phase
905        assert_eq!(session.phase, SpecPhase::Tasks);
906        spec.tasks.push(Task {
907            id: "1".to_string(),
908            description: "Implement task manager".to_string(),
909            subtasks: vec![],
910            requirements: vec!["REQ-1".to_string()],
911            status: crate::models::TaskStatus::NotStarted,
912            optional: false,
913        });
914
915        AISpecWriter::request_approval(&mut session, "reviewer", None).unwrap();
916        AISpecWriter::transition_to_next_phase(&mut session).unwrap();
917
918        // Execution phase
919        assert_eq!(session.phase, SpecPhase::Execution);
920    }
921
922    #[test]
923    fn test_conversation_history_preserved() {
924        let mut session = create_test_session();
925
926        for i in 0..10 {
927            AISpecWriter::add_message(&mut session, MessageRole::User, &format!("Message {}", i))
928                .unwrap();
929        }
930
931        let history = AISpecWriter::get_conversation_history(&session);
932        assert_eq!(history.len(), 10);
933
934        for (i, msg) in history.iter().enumerate() {
935            assert_eq!(msg.content, format!("Message {}", i));
936            assert!(msg.timestamp <= Utc::now());
937        }
938    }
939
940    #[test]
941    fn test_gap_analysis_suggestions_vary_by_phase() {
942        let spec = create_test_spec();
943
944        let mut session = create_test_session();
945        session.phase = SpecPhase::Discovery;
946        let discovery_gaps = AISpecWriter::analyze_gaps(&session, &spec);
947
948        session.phase = SpecPhase::Requirements;
949        let requirements_gaps = AISpecWriter::analyze_gaps(&session, &spec);
950
951        session.phase = SpecPhase::Design;
952        let design_gaps = AISpecWriter::analyze_gaps(&session, &spec);
953
954        // Each phase should have different suggestions
955        assert_ne!(discovery_gaps.suggestions, requirements_gaps.suggestions);
956        assert_ne!(requirements_gaps.suggestions, design_gaps.suggestions);
957    }
958
959    #[test]
960    fn test_build_prompt_with_steering_context() {
961        use crate::models::SteeringRule;
962
963        let session = create_test_session();
964        let global_steering = Steering {
965            rules: vec![SteeringRule {
966                id: "global-rule".to_string(),
967                description: "Global rule".to_string(),
968                pattern: "pattern".to_string(),
969                action: "enforce".to_string(),
970            }],
971            standards: vec![],
972            templates: vec![],
973        };
974
975        let project_steering = Steering {
976            rules: vec![SteeringRule {
977                id: "project-rule".to_string(),
978                description: "Project rule".to_string(),
979                pattern: "pattern".to_string(),
980                action: "enforce".to_string(),
981            }],
982            standards: vec![],
983            templates: vec![],
984        };
985
986        let result = AISpecWriter::build_prompt_with_steering_context(
987            &session,
988            &global_steering,
989            &project_steering,
990            "Create a task manager",
991        );
992
993        assert!(result.is_ok());
994        let prompt = result.unwrap();
995
996        // Verify prompt contains steering context
997        assert!(prompt.contains("Phase Guidance"));
998        assert!(prompt.contains("Steering Rules"));
999        assert!(prompt.contains("global-rule"));
1000        assert!(prompt.contains("project-rule"));
1001        assert!(prompt.contains("Create a task manager"));
1002    }
1003
1004    #[test]
1005    fn test_build_prompt_with_steering_context_includes_conversation() {
1006        let mut session = create_test_session();
1007        AISpecWriter::add_message(&mut session, MessageRole::User, "What should we build?")
1008            .unwrap();
1009        AISpecWriter::add_message(
1010            &mut session,
1011            MessageRole::Assistant,
1012            "Let's build a task manager",
1013        )
1014        .unwrap();
1015
1016        let global_steering = Steering {
1017            rules: vec![],
1018            standards: vec![],
1019            templates: vec![],
1020        };
1021
1022        let project_steering = Steering {
1023            rules: vec![],
1024            standards: vec![],
1025            templates: vec![],
1026        };
1027
1028        let result = AISpecWriter::build_prompt_with_steering_context(
1029            &session,
1030            &global_steering,
1031            &project_steering,
1032            "Continue with requirements",
1033        );
1034
1035        assert!(result.is_ok());
1036        let prompt = result.unwrap();
1037
1038        // Verify conversation history is included
1039        assert!(prompt.contains("Conversation History"));
1040        assert!(prompt.contains("What should we build?"));
1041        assert!(prompt.contains("Let's build a task manager"));
1042    }
1043
1044    #[test]
1045    fn test_build_prompt_with_steering_context_project_precedence() {
1046        use crate::models::SteeringRule;
1047
1048        let session = create_test_session();
1049        let global_steering = Steering {
1050            rules: vec![SteeringRule {
1051                id: "rule-1".to_string(),
1052                description: "Global version".to_string(),
1053                pattern: "global".to_string(),
1054                action: "warn".to_string(),
1055            }],
1056            standards: vec![],
1057            templates: vec![],
1058        };
1059
1060        let project_steering = Steering {
1061            rules: vec![SteeringRule {
1062                id: "rule-1".to_string(),
1063                description: "Project version".to_string(),
1064                pattern: "project".to_string(),
1065                action: "enforce".to_string(),
1066            }],
1067            standards: vec![],
1068            templates: vec![],
1069        };
1070
1071        let result = AISpecWriter::build_prompt_with_steering_context(
1072            &session,
1073            &global_steering,
1074            &project_steering,
1075            "Test",
1076        );
1077
1078        assert!(result.is_ok());
1079        let prompt = result.unwrap();
1080
1081        // Verify project version takes precedence
1082        assert!(prompt.contains("Project version"));
1083        assert!(prompt.contains("project"));
1084        assert!(!prompt.contains("Global version"));
1085    }
1086
1087    #[test]
1088    fn test_validate_spec_against_steering_valid() {
1089        let spec = Spec {
1090            id: "test".to_string(),
1091            name: "Test Spec".to_string(),
1092            version: "1.0.0".to_string(),
1093            requirements: vec![Requirement {
1094                id: "REQ-1".to_string(),
1095                user_story: "As a user".to_string(),
1096                acceptance_criteria: vec![AcceptanceCriterion {
1097                    id: "AC-1.1".to_string(),
1098                    when: "when".to_string(),
1099                    then: "then".to_string(),
1100                }],
1101                priority: Priority::Must,
1102            }],
1103            design: None,
1104            tasks: vec![],
1105            metadata: SpecMetadata {
1106                author: Some("Test Author".to_string()),
1107                created_at: Utc::now(),
1108                updated_at: Utc::now(),
1109                phase: SpecPhase::Requirements,
1110                status: SpecStatus::Draft,
1111            },
1112            inheritance: None,
1113        };
1114
1115        let steering = Steering {
1116            rules: vec![],
1117            standards: vec![],
1118            templates: vec![],
1119        };
1120
1121        let result = AISpecWriter::validate_spec_against_steering(&spec, &steering);
1122        assert!(result.is_ok());
1123        let violations = result.unwrap();
1124        assert!(violations.is_empty());
1125    }
1126
1127    #[test]
1128    fn test_validate_spec_against_steering_missing_author() {
1129        let spec = Spec {
1130            id: "test".to_string(),
1131            name: "Test Spec".to_string(),
1132            version: "1.0.0".to_string(),
1133            requirements: vec![],
1134            design: None,
1135            tasks: vec![],
1136            metadata: SpecMetadata {
1137                author: None,
1138                created_at: Utc::now(),
1139                updated_at: Utc::now(),
1140                phase: SpecPhase::Discovery,
1141                status: SpecStatus::Draft,
1142            },
1143            inheritance: None,
1144        };
1145
1146        let steering = Steering {
1147            rules: vec![],
1148            standards: vec![],
1149            templates: vec![],
1150        };
1151
1152        let result = AISpecWriter::validate_spec_against_steering(&spec, &steering);
1153        assert!(result.is_ok());
1154        let violations = result.unwrap();
1155        assert!(!violations.is_empty());
1156        assert!(violations.iter().any(|v| v.contains("author")));
1157    }
1158
1159    #[test]
1160    fn test_validate_spec_against_steering_requirements_phase_no_requirements() {
1161        let spec = Spec {
1162            id: "test".to_string(),
1163            name: "Test Spec".to_string(),
1164            version: "1.0.0".to_string(),
1165            requirements: vec![],
1166            design: None,
1167            tasks: vec![],
1168            metadata: SpecMetadata {
1169                author: Some("Author".to_string()),
1170                created_at: Utc::now(),
1171                updated_at: Utc::now(),
1172                phase: SpecPhase::Requirements,
1173                status: SpecStatus::Draft,
1174            },
1175            inheritance: None,
1176        };
1177
1178        let steering = Steering {
1179            rules: vec![],
1180            standards: vec![],
1181            templates: vec![],
1182        };
1183
1184        let result = AISpecWriter::validate_spec_against_steering(&spec, &steering);
1185        assert!(result.is_ok());
1186        let violations = result.unwrap();
1187        assert!(!violations.is_empty());
1188        assert!(violations.iter().any(|v| v.contains("Requirements phase")));
1189    }
1190
1191    #[test]
1192    fn test_format_steering_context() {
1193        use crate::models::{Standard, SteeringRule, TemplateRef};
1194
1195        let steering = Steering {
1196            rules: vec![SteeringRule {
1197                id: "rule-1".to_string(),
1198                description: "Use snake_case".to_string(),
1199                pattern: "^[a-z_]+$".to_string(),
1200                action: "enforce".to_string(),
1201            }],
1202            standards: vec![Standard {
1203                id: "std-1".to_string(),
1204                description: "Test all public APIs".to_string(),
1205            }],
1206            templates: vec![TemplateRef {
1207                id: "tpl-1".to_string(),
1208                path: "templates/entity.rs".to_string(),
1209            }],
1210        };
1211
1212        let output = AISpecWriter::format_steering_context(&steering);
1213
1214        assert!(output.contains("Steering Rules"));
1215        assert!(output.contains("rule-1"));
1216        assert!(output.contains("Standards"));
1217        assert!(output.contains("std-1"));
1218        assert!(output.contains("Templates"));
1219        assert!(output.contains("tpl-1"));
1220    }
1221
1222    #[test]
1223    fn test_format_steering_context_empty() {
1224        let steering = Steering {
1225            rules: vec![],
1226            standards: vec![],
1227            templates: vec![],
1228        };
1229
1230        let output = AISpecWriter::format_steering_context(&steering);
1231
1232        // Should be empty or minimal
1233        assert!(output.is_empty() || output.trim().is_empty());
1234    }
1235}