Skip to main content

foundry_mcp/core/ops/
load_spec.rs

1//! Core op for loading specs or listing available specs (tool-agnostic)
2
3use anyhow::{Context, Result};
4
5use crate::core::{foundry, spec};
6use crate::types::responses::{
7    FoundryResponse, LoadSpecResponse, SpecContent, SpecInfo, ValidationStatus,
8};
9
10#[derive(Debug, Clone)]
11pub struct Input {
12    pub project_name: String,
13    pub spec_name: Option<String>,
14}
15
16pub async fn run(input: Input) -> Result<FoundryResponse<LoadSpecResponse>> {
17    let foundry = foundry::get_default_foundry()?;
18
19    validate_project_exists(&foundry, &input.project_name).await?;
20
21    let project_summary = load_project_summary(&foundry, &input.project_name).await?;
22
23    match &input.spec_name {
24        None => {
25            let specs = foundry.list_specs(&input.project_name).await?;
26            let available_specs: Vec<SpecInfo> = specs
27                .into_iter()
28                .map(|spec_meta| SpecInfo {
29                    name: spec_meta.name,
30                    feature_name: spec_meta.feature_name,
31                    created_at: spec_meta.created_at,
32                })
33                .collect();
34
35            let response_data = LoadSpecResponse {
36                project_name: input.project_name.clone(),
37                project_summary,
38                spec_name: None,
39                created_at: None,
40                spec_content: None,
41                available_specs: available_specs.clone(),
42                match_info: None,
43            };
44
45            Ok(FoundryResponse {
46                data: response_data,
47                next_steps: generate_listing_next_steps(&input.project_name, &available_specs),
48                validation_status: if available_specs.is_empty() {
49                    ValidationStatus::Incomplete
50                } else {
51                    ValidationStatus::Complete
52                },
53                workflow_hints: generate_listing_workflow_hints(&available_specs),
54            })
55        }
56        Some(spec_name) => {
57            let match_strategy = foundry
58                .find_spec_match(&input.project_name, spec_name)
59                .await?;
60
61            let (spec_data, match_strategy) = match match_strategy {
62                spec::SpecMatchStrategy::None => {
63                    return Err(anyhow::anyhow!(
64                        "No spec found matching '{}' in project '{}'",
65                        spec_name,
66                        input.project_name
67                    ));
68                }
69                spec::SpecMatchStrategy::Multiple(candidates) => {
70                    return Err(anyhow::anyhow!(
71                        "Multiple specs match '{}': {}. Please be more specific.",
72                        spec_name,
73                        candidates.join(", ")
74                    ));
75                }
76                spec::SpecMatchStrategy::Exact(actual_name) => {
77                    let spec_data = foundry
78                        .load_spec(&input.project_name, &actual_name)
79                        .await
80                        .with_context(|| format!("Failed to load spec '{}'", actual_name))?;
81                    (spec_data, spec::SpecMatchStrategy::Exact(actual_name))
82                }
83                spec::SpecMatchStrategy::FeatureExact(actual_name) => {
84                    let spec_data = foundry
85                        .load_spec(&input.project_name, &actual_name)
86                        .await
87                        .with_context(|| format!("Failed to load spec '{}'", actual_name))?;
88                    (
89                        spec_data,
90                        spec::SpecMatchStrategy::FeatureExact(actual_name),
91                    )
92                }
93                spec::SpecMatchStrategy::FeatureFuzzy(actual_name) => {
94                    let spec_data = foundry
95                        .load_spec(&input.project_name, &actual_name)
96                        .await
97                        .with_context(|| format!("Failed to load spec '{}'", actual_name))?;
98                    (
99                        spec_data,
100                        spec::SpecMatchStrategy::FeatureFuzzy(actual_name),
101                    )
102                }
103                spec::SpecMatchStrategy::NameFuzzy(actual_name) => {
104                    let spec_data = foundry
105                        .load_spec(&input.project_name, &actual_name)
106                        .await
107                        .with_context(|| format!("Failed to load spec '{}'", actual_name))?;
108                    (spec_data, spec::SpecMatchStrategy::NameFuzzy(actual_name))
109                }
110            };
111
112            let spec_content = SpecContent {
113                content: spec_data.content,
114            };
115
116            let match_info = match match_strategy {
117                spec::SpecMatchStrategy::Exact(_) => None,
118                _ => Some(crate::types::responses::MatchInfo {
119                    requested_spec: spec_name.clone(),
120                    matched_spec: spec_data.name.clone(),
121                    match_type: match match_strategy {
122                        spec::SpecMatchStrategy::FeatureExact(_) => "feature_exact".to_string(),
123                        spec::SpecMatchStrategy::FeatureFuzzy(_) => "feature_fuzzy".to_string(),
124                        spec::SpecMatchStrategy::NameFuzzy(_) => "name_fuzzy".to_string(),
125                        _ => "exact".to_string(),
126                    },
127                    confidence: 1.0,
128                }),
129            };
130
131            let response_data = LoadSpecResponse {
132                project_name: input.project_name.clone(),
133                project_summary,
134                spec_name: Some(spec_data.name.clone()),
135                created_at: Some(spec_data.created_at.clone()),
136                spec_content: Some(spec_content),
137                available_specs: Vec::new(),
138                match_info,
139            };
140
141            Ok(FoundryResponse {
142                data: response_data,
143                next_steps: generate_spec_next_steps(&input.project_name, &spec_data.name),
144                validation_status: ValidationStatus::Complete,
145                workflow_hints: generate_spec_workflow_hints(&spec_data.name),
146            })
147        }
148    }
149}
150
151async fn validate_project_exists(
152    foundry: &foundry::Foundry<crate::core::backends::filesystem::FilesystemBackend>,
153    project_name: &str,
154) -> Result<()> {
155    if !foundry.project_exists(project_name).await? {
156        return Err(anyhow::anyhow!(
157            "Project '{}' not found. Use 'mcp_foundry_list_projects' to see available projects.",
158            project_name
159        ));
160    }
161    Ok(())
162}
163
164async fn load_project_summary(
165    foundry: &foundry::Foundry<crate::core::backends::filesystem::FilesystemBackend>,
166    project_name: &str,
167) -> Result<String> {
168    let project = foundry.load_project(project_name).await?;
169
170    Ok(project.summary.unwrap_or_else(|| {
171        "No project summary available. Consider updating the project summary for better context.".to_string()
172    }))
173}
174
175fn generate_listing_next_steps(project_name: &str, available_specs: &[SpecInfo]) -> Vec<String> {
176    if available_specs.is_empty() {
177        vec![
178            "No specifications found for this project - ready for specification creation"
179                .to_string(),
180            format!(
181                "You can create your first specification: mcp_foundry_create_spec {} <feature_name>",
182                project_name
183            ),
184            "You can use 'mcp_foundry_load_project' to see full project context".to_string(),
185        ]
186    } else {
187        let mut steps = vec![
188            format!(
189                "Found {} specification(s) in project",
190                available_specs.len()
191            ),
192            format!(
193                "You can load a specific spec: mcp_foundry_load_spec {} <spec_name>",
194                project_name
195            ),
196        ];
197
198        if available_specs.len() <= 5 {
199            steps.push("Available specs:".to_string());
200            for spec in available_specs {
201                steps.push(format!("  - {} ({})", spec.name, spec.feature_name));
202            }
203        }
204
205        steps.push(format!(
206            "You can create a new spec: mcp_foundry_create_spec {} <feature_name>",
207            project_name
208        ));
209
210        steps
211    }
212}
213
214fn generate_spec_next_steps(project_name: &str, spec_name: &str) -> Vec<String> {
215    vec![
216        format!("Spec '{}' loaded successfully", spec_name),
217        "You can review the specification content and tasks for implementation guidance"
218            .to_string(),
219        "You can use the project summary for additional context".to_string(),
220        format!(
221            "You can create a new spec: mcp_foundry_create_spec {} <feature_name>",
222            project_name
223        ),
224        format!(
225            "You can list all specs: mcp_foundry_load_spec {}",
226            project_name
227        ),
228    ]
229}
230
231fn generate_listing_workflow_hints(available_specs: &[SpecInfo]) -> Vec<String> {
232    let mut hints = vec![
233        "You can use the project summary for context about all specifications".to_string(),
234        "Specifications are timestamped and organized by feature for easy navigation".to_string(),
235    ];
236
237    if available_specs.is_empty() {
238        hints.push(
239            "You can start by creating specifications to track development features".to_string(),
240        );
241        hints.push(
242            "Each spec includes implementation notes and task lists for comprehensive planning"
243                .to_string(),
244        );
245    } else {
246        hints.push(format!("Total specs: {}", available_specs.len()));
247        hints
248            .push("You can load individual specs to see detailed implementation plans".to_string());
249        hints.push(
250            "Specs include specification content, notes, and task lists for complete context"
251                .to_string(),
252        );
253    }
254
255    hints
256}
257
258fn generate_spec_workflow_hints(spec_name: &str) -> Vec<String> {
259    vec![
260        format!("Loaded spec: {}", spec_name),
261        "You must update task-list.md as work progresses".to_string(),
262        "You can add notes for design decisions and implementation details".to_string(),
263        "Spec content provides detailed feature requirements and acceptance criteria".to_string(),
264        "You can use the project summary for broader context during implementation".to_string(),
265    ]
266}