1use crate::core::backends::{FoundryBackend, SpecContentStore};
4use crate::core::edit_engine::{EditCommandsResult, EditEngine};
5use crate::core::spec::SpecMatchStrategy;
6use crate::types::edit_commands::EditCommand;
7use crate::types::{
8 project::{Project, ProjectConfig, ProjectMetadata},
9 spec::{Spec, SpecConfig, SpecFileType, SpecMetadata},
10};
11use anyhow::Result;
12
13pub struct Foundry<B: FoundryBackend> {
15 backend: B,
16}
17
18impl<B: FoundryBackend> Foundry<B> {
19 pub fn new(backend: B) -> Self {
20 Self { backend }
21 }
22
23 pub async fn create_project(&self, config: ProjectConfig) -> Result<Project> {
25 self.backend.create_project(config).await
26 }
27
28 pub async fn project_exists(&self, name: &str) -> Result<bool> {
29 self.backend.project_exists(name).await
30 }
31
32 pub async fn list_projects(&self) -> Result<Vec<ProjectMetadata>> {
33 self.backend.list_projects().await
34 }
35
36 pub async fn load_project(&self, name: &str) -> Result<Project> {
37 self.backend.load_project(name).await
38 }
39
40 pub async fn create_spec(&self, config: SpecConfig) -> Result<Spec> {
42 self.backend.create_spec(config).await
43 }
44
45 pub async fn list_specs(&self, project_name: &str) -> Result<Vec<SpecMetadata>> {
46 self.backend.list_specs(project_name).await
47 }
48
49 pub async fn load_spec(&self, project_name: &str, spec_name: &str) -> Result<Spec> {
50 self.backend.load_spec(project_name, spec_name).await
51 }
52
53 pub async fn update_spec_content(
54 &self,
55 project_name: &str,
56 spec_name: &str,
57 file_type: SpecFileType,
58 content: &str,
59 ) -> Result<()> {
60 self.backend
61 .update_spec_content(project_name, spec_name, file_type, content)
62 .await
63 }
64
65 pub async fn delete_spec(&self, project_name: &str, spec_name: &str) -> Result<()> {
66 self.backend.delete_spec(project_name, spec_name).await
67 }
68
69 pub async fn get_latest_spec(&self, project_name: &str) -> Result<Option<SpecMetadata>> {
71 self.backend.get_latest_spec(project_name).await
72 }
73
74 pub async fn count_specs(&self, project_name: &str) -> Result<usize> {
75 self.backend.count_specs(project_name).await
76 }
77
78 pub fn generate_spec_name(feature_name: &str) -> String {
80 use chrono::{Datelike, Timelike, Utc};
82 let now = Utc::now();
83 format!(
84 "{:04}{:02}{:02}_{:02}{:02}{:02}_{}",
85 now.year(),
86 now.month(),
87 now.day(),
88 now.hour(),
89 now.minute(),
90 now.second(),
91 feature_name
92 )
93 }
94
95 pub fn validate_spec_name(spec_name: &str) -> Result<()> {
96 use crate::utils::timestamp;
98
99 if timestamp::parse_spec_timestamp(spec_name).is_none() {
100 return Err(anyhow::anyhow!(
101 "Invalid spec name format. Expected: YYYYMMDD_HHMMSS_feature_name, got: {}",
102 spec_name
103 ));
104 }
105
106 if let Some(feature_name) = timestamp::extract_feature_name(spec_name) {
108 if feature_name.is_empty() {
109 return Err(anyhow::anyhow!(
110 "Spec name must include a feature name after the timestamp"
111 ));
112 }
113
114 if !feature_name
116 .chars()
117 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
118 || feature_name.starts_with('_')
119 || feature_name.ends_with('_')
120 || feature_name.contains("__")
121 {
122 return Err(anyhow::anyhow!(
123 "Feature name must be in snake_case format: {}",
124 feature_name
125 ));
126 }
127 } else {
128 return Err(anyhow::anyhow!(
129 "Could not extract feature name from spec name: {}",
130 spec_name
131 ));
132 }
133
134 Ok(())
135 }
136
137 pub async fn find_spec_match(
138 &self,
139 project_name: &str,
140 query: &str,
141 ) -> Result<SpecMatchStrategy> {
142 use strsim;
144
145 if query.trim().is_empty() {
147 return Err(anyhow::anyhow!("Query cannot be empty"));
148 }
149
150 if project_name.trim().is_empty() {
151 return Err(anyhow::anyhow!("Project name cannot be empty"));
152 }
153
154 let available_specs = self.list_specs(project_name).await?;
155
156 if available_specs.is_empty() {
157 return Ok(SpecMatchStrategy::None);
158 }
159
160 if let Some(exact_match) = available_specs.iter().find(|s| s.name == query) {
162 return Ok(SpecMatchStrategy::Exact(exact_match.name.clone()));
163 }
164
165 if let Some(feature_match) = available_specs.iter().find(|s| s.feature_name == query) {
167 return Ok(SpecMatchStrategy::FeatureExact(feature_match.name.clone()));
168 }
169
170 let query_lower = query.to_lowercase();
172 let substring_matches: Vec<&SpecMetadata> = available_specs
173 .iter()
174 .filter(|s| s.feature_name.to_lowercase().contains(&query_lower))
175 .collect();
176
177 if substring_matches.len() == 1 {
178 return Ok(SpecMatchStrategy::FeatureFuzzy(
179 substring_matches[0].name.clone(),
180 ));
181 } else if substring_matches.len() > 1 {
182 let mut names: Vec<String> = substring_matches
184 .into_iter()
185 .map(|s| s.name.clone())
186 .collect();
187 names.sort();
188 return Ok(SpecMatchStrategy::Multiple(names));
189 }
190
191 let feature_matches: Vec<(String, f32)> = available_specs
193 .iter()
194 .map(|s| {
195 let similarity = strsim::normalized_levenshtein(query, &s.feature_name) as f32;
196 (s.name.clone(), similarity)
197 })
198 .filter(|(_, confidence)| *confidence > 0.8) .collect();
200
201 if feature_matches.len() == 1 {
202 return Ok(SpecMatchStrategy::FeatureFuzzy(
203 feature_matches[0].0.clone(),
204 ));
205 } else if feature_matches.len() > 1 {
206 let mut names: Vec<String> =
208 feature_matches.into_iter().map(|(name, _)| name).collect();
209 names.sort();
210 return Ok(SpecMatchStrategy::Multiple(names));
211 }
212
213 let name_matches: Vec<(String, f32)> = available_specs
215 .iter()
216 .map(|s| {
217 let similarity = strsim::normalized_levenshtein(query, &s.name) as f32;
218 (s.name.clone(), similarity)
219 })
220 .filter(|(_, confidence)| *confidence > 0.8) .collect();
222
223 if name_matches.len() == 1 {
224 return Ok(SpecMatchStrategy::NameFuzzy(name_matches[0].0.clone()));
225 } else if name_matches.len() > 1 {
226 let mut names: Vec<String> = name_matches.into_iter().map(|(name, _)| name).collect();
228 names.sort();
229 return Ok(SpecMatchStrategy::Multiple(names));
230 }
231
232 Ok(SpecMatchStrategy::None)
233 }
234
235 pub async fn apply_edit_commands(
237 &self,
238 project_name: &str,
239 spec_name: &str,
240 commands: &[EditCommand],
241 ) -> Result<EditCommandsResult> {
242 EditEngine::apply_edit_commands_with_store(project_name, spec_name, commands, self).await
243 }
244}
245
246#[async_trait::async_trait]
248impl<B: FoundryBackend> SpecContentStore for Foundry<B> {
249 async fn read_spec_file(
250 &self,
251 project_name: &str,
252 spec_name: &str,
253 file_type: SpecFileType,
254 ) -> Result<String> {
255 let spec = self.load_spec(project_name, spec_name).await?;
257
258 let content = match file_type {
259 SpecFileType::Spec => spec.content.spec,
260 SpecFileType::Notes => spec.content.notes,
261 SpecFileType::TaskList => spec.content.tasks,
262 };
263
264 Ok(content)
265 }
266
267 async fn write_spec_file(
268 &self,
269 project_name: &str,
270 spec_name: &str,
271 file_type: SpecFileType,
272 content: &str,
273 ) -> Result<()> {
274 self.update_spec_content(project_name, spec_name, file_type, content)
275 .await
276 }
277
278 async fn is_file_modified(
279 &self,
280 project_name: &str,
281 spec_name: &str,
282 file_type: SpecFileType,
283 new_content: &str,
284 ) -> Result<bool> {
285 let current_content = self
286 .read_spec_file(project_name, spec_name, file_type)
287 .await?;
288 Ok(current_content != new_content)
289 }
290}
291
292pub fn get_default_foundry() -> Result<Foundry<crate::core::backends::filesystem::FilesystemBackend>>
296{
297 let backend = crate::core::backends::filesystem::FilesystemBackend::new();
298 Ok(Foundry::new(backend))
299}