1use 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}