foundry_mcp/cli/commands/
create_spec.rs1use 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(&args.project_name)?;
13
14 validate_feature_name(&args.feature_name)?;
16
17 let content_validation = validate_content(&args)?;
19 let has_validation_warnings = content_validation
20 .iter()
21 .any(|(_, result)| !result.is_valid);
22
23 let spec_config = build_spec_config(args);
25 let created_spec = spec::create_spec(spec_config).context("Failed to create specification")?;
26
27 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
57fn 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
68fn validate_feature_name(feature_name: &str) -> Result<()> {
70 paths::validate_feature_name(feature_name).context("Feature name validation failed")
71}
72
73fn 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
95fn 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
108fn 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
124fn 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 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 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 "", "Feature-Name", "featureName", "feature name", "feature.name", "feature__name", "_feature", "feature_", "FEATURE_NAME", ];
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 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 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}