Skip to main content

foundry_mcp/core/ops/
create_spec.rs

1//! Core op for creating a spec (tool-agnostic)
2
3use anyhow::{Context, Result};
4
5use crate::core::{foundry, validation};
6use crate::types::responses::{CreateSpecResponse, FoundryResponse, ValidationStatus};
7use crate::types::spec::{SpecConfig, SpecContentData};
8use crate::utils::paths;
9
10/// Input for create_spec operation (decoupled from interface-specific args)
11#[derive(Debug, Clone)]
12pub struct Input {
13    pub project_name: String,
14    pub feature_name: String,
15    pub spec: String,
16    pub notes: String,
17    pub tasks: String,
18}
19
20/// Execute the create_spec operation and return a structured response
21pub async fn run(input: Input) -> Result<FoundryResponse<CreateSpecResponse>> {
22    let foundry = foundry::get_default_foundry()?;
23
24    // Validate project exists
25    validate_project_exists(&foundry, &input.project_name).await?;
26
27    // Validate feature name
28    validate_feature_name(&input.feature_name)?;
29
30    // Validate content
31    let content_validation = validate_content(&input)?;
32    let has_validation_warnings = content_validation
33        .iter()
34        .any(|(_, result)| !result.is_valid);
35
36    // Create the spec
37    let spec_config = build_spec_config(input);
38    let created_spec = foundry
39        .create_spec(spec_config)
40        .await
41        .context("Failed to create specification")?;
42
43    // Build response
44    let response_data = CreateSpecResponse {
45        project_name: created_spec.project_name.clone(),
46        spec_name: created_spec.name.clone(),
47        created_at: created_spec.created_at.clone(),
48        spec_path: created_spec.path.to_string_lossy().to_string(),
49        files_created: vec![
50            format!("{}/spec.md", created_spec.name),
51            format!("{}/notes.md", created_spec.name),
52            format!("{}/task-list.md", created_spec.name),
53        ],
54    };
55
56    let validation_status = if has_validation_warnings {
57        ValidationStatus::Incomplete
58    } else {
59        ValidationStatus::Complete
60    };
61
62    let next_steps = generate_next_steps(&created_spec.project_name, &created_spec.name);
63    let workflow_hints = generate_workflow_hints(&content_validation);
64
65    Ok(FoundryResponse {
66        data: response_data,
67        next_steps,
68        validation_status,
69        workflow_hints,
70    })
71}
72
73/// Validate that project exists
74async fn validate_project_exists(
75    foundry: &foundry::Foundry<crate::core::backends::filesystem::FilesystemBackend>,
76    project_name: &str,
77) -> Result<()> {
78    if !foundry.project_exists(project_name).await? {
79        return Err(anyhow::anyhow!(
80            "Project '{}' not found. Use list_projects via MCP to see available projects: {{\"name\": \"list_projects\", \"arguments\": {{}}}}",
81            project_name
82        ));
83    }
84    Ok(())
85}
86
87/// Validate feature name format
88fn validate_feature_name(feature_name: &str) -> Result<()> {
89    paths::validate_feature_name(feature_name).context("Feature name validation failed")
90}
91
92/// Validate content according to schema requirements
93fn validate_content(input: &Input) -> Result<Vec<(&'static str, validation::ValidationResult)>> {
94    let validations = vec![
95        (
96            "Spec Content",
97            validation::validate_content(validation::ContentType::Spec, &input.spec),
98        ),
99        (
100            "Implementation Notes",
101            validation::validate_content(validation::ContentType::Notes, &input.notes),
102        ),
103        (
104            "Task List",
105            validation::validate_content(validation::ContentType::Tasks, &input.tasks),
106        ),
107    ];
108
109    Ok(validations)
110}
111
112/// Build spec config from input
113fn build_spec_config(input: Input) -> SpecConfig {
114    SpecConfig {
115        project_name: input.project_name,
116        feature_name: input.feature_name,
117        content: SpecContentData {
118            spec: input.spec,
119            notes: input.notes,
120            tasks: input.tasks,
121        },
122    }
123}
124
125/// Generate next steps for the response
126fn generate_next_steps(project_name: &str, spec_name: &str) -> Vec<String> {
127    vec![
128        format!(
129            "Specification '{}' created successfully from your provided content",
130            spec_name
131        ),
132        "Your specification content has been structured and is ready for implementation work"
133            .to_string(),
134        format!(
135            "Load spec: {{\"name\": \"load_spec\", \"arguments\": {{\"project_name\": \"{}\", \"spec_name\": \"{}\"}}}}; Load project: {{\"name\": \"load_project\", \"arguments\": {{\"project_name\": \"{}\"}}}}",
136            project_name, spec_name, project_name
137        ),
138    ]
139}
140
141/// Generate workflow hints based on validation results
142fn generate_workflow_hints(
143    validation_results: &[(&'static str, validation::ValidationResult)],
144) -> Vec<String> {
145    let mut hints = vec![
146        "📋 DOCUMENT PURPOSE: Your spec content serves as COMPLETE CONTEXT for future implementation".to_string(),
147        "🎯 CONTEXT TEST: Could someone with no prior knowledge implement this feature using only your spec documents?".to_string(),
148        "Your specification content has been structured with task-list.md for implementation tracking".to_string(),
149        "Review spec: {\"name\": \"load_spec\", \"arguments\": {\"project_name\": \"<project>\", \"spec_name\": \"<spec>\"}}".to_string(),
150        "Load project: {\"name\": \"load_project\", \"arguments\": {\"project_name\": \"<project>\"}}".to_string(),
151    ];
152
153    // Add validation-specific hints
154    let invalid_content: Vec<&str> = validation_results
155        .iter()
156        .filter_map(|(name, result)| if !result.is_valid { Some(*name) } else { None })
157        .collect();
158
159    if !invalid_content.is_empty() {
160        hints.push(format!(
161            "You might consider reviewing content quality for: {}",
162            invalid_content.join(", ")
163        ));
164    }
165
166    hints.push("Tool selection guidance: {\"name\": \"get_foundry_help\", \"arguments\": {\"topic\": \"decision-points\"}}".to_string());
167
168    hints
169}