foundry_mcp/cli/commands/
create_spec.rs

1//! Implementation of the create_spec command
2
3use crate::cli::args::CreateSpecArgs;
4use crate::core::{project, spec, validation};
5use crate::types::responses::{CreateSpecResponse, FoundryResponse, ValidationStatus};
6use crate::types::spec::{SpecConfig, SpecContentData};
7use crate::utils::paths;
8use anyhow::{Context, Result};
9
10pub async fn execute(args: CreateSpecArgs) -> Result<FoundryResponse<CreateSpecResponse>> {
11    // Validate project exists
12    validate_project_exists(&args.project_name)?;
13
14    // Validate feature name
15    validate_feature_name(&args.feature_name)?;
16
17    // Validate content
18    let content_validation = validate_content(&args)?;
19    let has_validation_warnings = content_validation
20        .iter()
21        .any(|(_, result)| !result.is_valid);
22
23    // Create the spec
24    let spec_config = build_spec_config(args);
25    let created_spec = spec::create_spec(spec_config).context("Failed to create specification")?;
26
27    // Build response
28    let response_data = CreateSpecResponse {
29        project_name: created_spec.project_name.clone(),
30        spec_name: created_spec.name.clone(),
31        created_at: created_spec.created_at.clone(),
32        spec_path: created_spec.path.to_string_lossy().to_string(),
33        files_created: vec![
34            format!("{}/spec.md", created_spec.name),
35            format!("{}/notes.md", created_spec.name),
36            format!("{}/task-list.md", created_spec.name),
37        ],
38    };
39
40    let validation_status = if has_validation_warnings {
41        ValidationStatus::Incomplete
42    } else {
43        ValidationStatus::Complete
44    };
45
46    let next_steps = generate_next_steps(&created_spec.project_name, &created_spec.name);
47    let workflow_hints = generate_workflow_hints(&content_validation);
48
49    Ok(FoundryResponse {
50        data: response_data,
51        next_steps,
52        validation_status,
53        workflow_hints,
54    })
55}
56
57/// Validate that project exists
58fn validate_project_exists(project_name: &str) -> Result<()> {
59    if !project::project_exists(project_name)? {
60        return Err(anyhow::anyhow!(
61            "Project '{}' not found. Use list_projects via MCP to see available projects: {{\"name\": \"list_projects\", \"arguments\": {{}}}}",
62            project_name
63        ));
64    }
65    Ok(())
66}
67
68/// Validate feature name format
69fn validate_feature_name(feature_name: &str) -> Result<()> {
70    paths::validate_feature_name(feature_name).context("Feature name validation failed")
71}
72
73/// Validate content according to schema requirements
74fn validate_content(
75    args: &CreateSpecArgs,
76) -> Result<Vec<(&'static str, validation::ValidationResult)>> {
77    let validations = vec![
78        (
79            "Spec Content",
80            validation::validate_content(validation::ContentType::Spec, &args.spec),
81        ),
82        (
83            "Implementation Notes",
84            validation::validate_content(validation::ContentType::Notes, &args.notes),
85        ),
86        (
87            "Task List",
88            validation::validate_content(validation::ContentType::Tasks, &args.tasks),
89        ),
90    ];
91
92    Ok(validations)
93}
94
95/// Build spec config from CLI arguments
96fn build_spec_config(args: CreateSpecArgs) -> SpecConfig {
97    SpecConfig {
98        project_name: args.project_name,
99        feature_name: args.feature_name,
100        content: SpecContentData {
101            spec: args.spec,
102            notes: args.notes,
103            tasks: args.tasks,
104        },
105    }
106}
107
108/// Generate next steps for the response
109fn generate_next_steps(project_name: &str, spec_name: &str) -> Vec<String> {
110    vec![
111        format!(
112            "Specification '{}' created successfully from your provided content",
113            spec_name
114        ),
115        "Your specification content has been structured and is ready for implementation work"
116            .to_string(),
117        format!(
118            "Load spec: {{\"name\": \"load_spec\", \"arguments\": {{\"project_name\": \"{}\", \"spec_name\": \"{}\"}}}}; Load project: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}",
119            project_name, spec_name, project_name
120        ),
121    ]
122}
123
124/// Generate workflow hints based on validation results
125fn generate_workflow_hints(
126    validation_results: &[(&'static str, validation::ValidationResult)],
127) -> Vec<String> {
128    let mut hints = vec![
129        "📋 DOCUMENT PURPOSE: Your spec content serves as COMPLETE CONTEXT for future implementation".to_string(),
130        "🎯 CONTEXT TEST: Could someone with no prior knowledge implement this feature using only your spec documents?".to_string(),
131        "Your specification content has been structured with task-list.md for implementation tracking".to_string(),
132        "Review spec: {\"name\": \"load_spec\", \"arguments\": {\"project_name\": \"<project>\", \"spec_name\": \"<spec>\"}}".to_string(),
133        "Load project: {\"name\": \"load_project\", \"arguments\": {\"project_name\": \"<project>\"}}".to_string(),
134    ];
135
136    // Add validation-specific hints
137    let invalid_content: Vec<&str> = validation_results
138        .iter()
139        .filter_map(|(name, result)| if !result.is_valid { Some(*name) } else { None })
140        .collect();
141
142    if !invalid_content.is_empty() {
143        hints.push(format!(
144            "You might consider reviewing content quality for: {}",
145            invalid_content.join(", ")
146        ));
147    }
148
149    hints.push("Tool selection guidance: {\"name\": \"get_foundry_help\", \"arguments\": {\"topic\": \"decision-points\"}}".to_string());
150
151    hints
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    // Create test arguments for spec creation
159    fn create_test_spec_args() -> CreateSpecArgs {
160        CreateSpecArgs {
161            project_name: "test-project".to_string(),
162            feature_name: "user_authentication".to_string(),
163            spec: "Implement user authentication system with JWT tokens. Users should be able to register, login, logout, and reset passwords. The system should include email verification and role-based access control with proper security measures.".to_string(),
164            notes: "Consider using bcrypt for password hashing. JWT tokens should expire after 24 hours. Need to implement rate limiting for login attempts and proper session management.".to_string(),
165            tasks: "Create user registration endpoint, Implement password hashing with bcrypt, Add JWT token generation and validation, Create login/logout endpoints, Implement email verification system, Add role-based middleware for access control, Create password reset flow with email verification".to_string(),
166        }
167    }
168
169    #[test]
170    fn test_validate_feature_name_valid() {
171        let valid_names = vec![
172            "user_authentication",
173            "api_endpoints",
174            "database_schema",
175            "test_feature",
176            "feature123",
177        ];
178
179        for name in valid_names {
180            assert!(
181                validate_feature_name(name).is_ok(),
182                "Feature name '{}' should be valid",
183                name
184            );
185        }
186    }
187
188    #[test]
189    fn test_validate_feature_name_invalid() {
190        let invalid_names = vec![
191            "",              // empty
192            "Feature-Name",  // kebab-case instead of snake_case
193            "featureName",   // camelCase
194            "feature name",  // spaces
195            "feature.name",  // dots
196            "feature__name", // double underscores
197            "_feature",      // starts with underscore
198            "feature_",      // ends with underscore
199            "FEATURE_NAME",  // all uppercase
200        ];
201
202        for name in invalid_names {
203            assert!(
204                validate_feature_name(name).is_err(),
205                "Feature name '{}' should be invalid",
206                name
207            );
208        }
209    }
210
211    #[test]
212    fn test_validate_content_structure() {
213        let args = create_test_spec_args();
214        let validations = validate_content(&args).unwrap();
215
216        assert_eq!(validations.len(), 3);
217
218        // Check that all content types are present
219        let content_types: Vec<&str> = validations.iter().map(|(t, _)| *t).collect();
220        assert!(content_types.contains(&"Spec Content"));
221        assert!(content_types.contains(&"Implementation Notes"));
222        assert!(content_types.contains(&"Task List"));
223    }
224
225    #[test]
226    fn test_validate_project_exists_missing_project() {
227        // Test with a project that definitely doesn't exist
228        let result = validate_project_exists("non-existent-project-12345");
229
230        assert!(result.is_err());
231        let error_message = result.unwrap_err().to_string();
232        assert!(error_message.contains("not found"));
233        assert!(error_message.contains("list_projects"));
234    }
235}