Skip to main content

agentic_forge_core/engine/
query.rs

1//! QueryEngine — read-only operations for blueprints.
2
3use crate::engine::ForgeEngine;
4use crate::types::blueprint::*;
5use crate::types::ids::*;
6use crate::types::{ForgeError, ForgeResult};
7
8pub struct QueryEngine<'a> {
9    engine: &'a ForgeEngine,
10}
11
12impl<'a> QueryEngine<'a> {
13    pub fn new(engine: &'a ForgeEngine) -> Self {
14        Self { engine }
15    }
16
17    // Blueprint queries
18
19    pub fn get_blueprint(&self, id: &BlueprintId) -> ForgeResult<&Blueprint> {
20        self.engine.store.load(id)
21    }
22
23    pub fn list_blueprints(&self) -> Vec<&Blueprint> {
24        self.engine.store.list()
25    }
26
27    pub fn list_by_status(&self, status: BlueprintStatus) -> Vec<&Blueprint> {
28        self.engine.store.list_by_status(status)
29    }
30
31    pub fn search_blueprints(&self, query: &str) -> Vec<&Blueprint> {
32        self.engine
33            .store
34            .list()
35            .into_iter()
36            .filter(|bp| {
37                bp.name.to_lowercase().contains(&query.to_lowercase())
38                    || bp
39                        .description
40                        .to_lowercase()
41                        .contains(&query.to_lowercase())
42            })
43            .collect()
44    }
45
46    pub fn blueprint_count(&self) -> usize {
47        self.engine.store.count()
48    }
49
50    pub fn blueprint_exists(&self, id: &BlueprintId) -> bool {
51        self.engine.store.contains(id)
52    }
53
54    // Entity queries
55
56    pub fn get_entity(&self, bp_id: &BlueprintId, entity_id: &EntityId) -> ForgeResult<&Entity> {
57        let bp = self.engine.store.load(bp_id)?;
58        bp.entities
59            .iter()
60            .find(|e| e.id == *entity_id)
61            .ok_or_else(|| ForgeError::EntityNotFound(entity_id.to_string()))
62    }
63
64    pub fn get_entity_by_name(&self, bp_id: &BlueprintId, name: &str) -> ForgeResult<&Entity> {
65        let bp = self.engine.store.load(bp_id)?;
66        bp.find_entity(name)
67            .ok_or_else(|| ForgeError::EntityNotFound(name.to_string()))
68    }
69
70    pub fn list_entities(&self, bp_id: &BlueprintId) -> ForgeResult<&[Entity]> {
71        let bp = self.engine.store.load(bp_id)?;
72        Ok(&bp.entities)
73    }
74
75    pub fn entity_count(&self, bp_id: &BlueprintId) -> ForgeResult<usize> {
76        let bp = self.engine.store.load(bp_id)?;
77        Ok(bp.entity_count())
78    }
79
80    pub fn search_entities(&self, bp_id: &BlueprintId, query: &str) -> ForgeResult<Vec<&Entity>> {
81        let bp = self.engine.store.load(bp_id)?;
82        Ok(bp
83            .entities
84            .iter()
85            .filter(|e| {
86                e.name.to_lowercase().contains(&query.to_lowercase())
87                    || e.description.to_lowercase().contains(&query.to_lowercase())
88            })
89            .collect())
90    }
91
92    pub fn list_aggregate_roots(&self, bp_id: &BlueprintId) -> ForgeResult<Vec<&Entity>> {
93        let bp = self.engine.store.load(bp_id)?;
94        Ok(bp.entities.iter().filter(|e| e.is_aggregate_root).collect())
95    }
96
97    // File queries
98
99    pub fn get_file(&self, bp_id: &BlueprintId, file_id: &FileId) -> ForgeResult<&FileBlueprint> {
100        let bp = self.engine.store.load(bp_id)?;
101        bp.files
102            .iter()
103            .find(|f| f.id == *file_id)
104            .ok_or_else(|| ForgeError::FileNotFound(file_id.to_string()))
105    }
106
107    pub fn get_file_by_path(&self, bp_id: &BlueprintId, path: &str) -> ForgeResult<&FileBlueprint> {
108        let bp = self.engine.store.load(bp_id)?;
109        bp.find_file(path)
110            .ok_or_else(|| ForgeError::FileNotFound(path.to_string()))
111    }
112
113    pub fn list_files(&self, bp_id: &BlueprintId) -> ForgeResult<&[FileBlueprint]> {
114        let bp = self.engine.store.load(bp_id)?;
115        Ok(&bp.files)
116    }
117
118    pub fn list_files_by_type(
119        &self,
120        bp_id: &BlueprintId,
121        ft: FileType,
122    ) -> ForgeResult<Vec<&FileBlueprint>> {
123        let bp = self.engine.store.load(bp_id)?;
124        Ok(bp.files.iter().filter(|f| f.file_type == ft).collect())
125    }
126
127    pub fn file_count(&self, bp_id: &BlueprintId) -> ForgeResult<usize> {
128        let bp = self.engine.store.load(bp_id)?;
129        Ok(bp.file_count())
130    }
131
132    // Dependency queries
133
134    pub fn get_dependency(
135        &self,
136        bp_id: &BlueprintId,
137        dep_id: &DependencyId,
138    ) -> ForgeResult<&Dependency> {
139        let bp = self.engine.store.load(bp_id)?;
140        bp.dependencies
141            .iter()
142            .find(|d| d.id == *dep_id)
143            .ok_or_else(|| ForgeError::DependencyNotFound(dep_id.to_string()))
144    }
145
146    pub fn get_dependency_by_name(
147        &self,
148        bp_id: &BlueprintId,
149        name: &str,
150    ) -> ForgeResult<&Dependency> {
151        let bp = self.engine.store.load(bp_id)?;
152        bp.find_dependency(name)
153            .ok_or_else(|| ForgeError::DependencyNotFound(name.to_string()))
154    }
155
156    pub fn list_dependencies(&self, bp_id: &BlueprintId) -> ForgeResult<&[Dependency]> {
157        let bp = self.engine.store.load(bp_id)?;
158        Ok(&bp.dependencies)
159    }
160
161    pub fn list_dependencies_by_type(
162        &self,
163        bp_id: &BlueprintId,
164        dt: DependencyType,
165    ) -> ForgeResult<Vec<&Dependency>> {
166        let bp = self.engine.store.load(bp_id)?;
167        Ok(bp
168            .dependencies
169            .iter()
170            .filter(|d| d.dep_type == dt)
171            .collect())
172    }
173
174    pub fn dependency_count(&self, bp_id: &BlueprintId) -> ForgeResult<usize> {
175        let bp = self.engine.store.load(bp_id)?;
176        Ok(bp.dependency_count())
177    }
178
179    // Test queries
180
181    pub fn get_test_case(&self, bp_id: &BlueprintId, tc_id: &TestCaseId) -> ForgeResult<&TestCase> {
182        let bp = self.engine.store.load(bp_id)?;
183        bp.test_cases
184            .iter()
185            .find(|t| t.id == *tc_id)
186            .ok_or_else(|| ForgeError::TestCaseNotFound(tc_id.to_string()))
187    }
188
189    pub fn list_test_cases(&self, bp_id: &BlueprintId) -> ForgeResult<&[TestCase]> {
190        let bp = self.engine.store.load(bp_id)?;
191        Ok(&bp.test_cases)
192    }
193
194    pub fn list_tests_by_type(
195        &self,
196        bp_id: &BlueprintId,
197        tt: TestType,
198    ) -> ForgeResult<Vec<&TestCase>> {
199        let bp = self.engine.store.load(bp_id)?;
200        Ok(bp.test_cases.iter().filter(|t| t.test_type == tt).collect())
201    }
202
203    pub fn test_count(&self, bp_id: &BlueprintId) -> ForgeResult<usize> {
204        let bp = self.engine.store.load(bp_id)?;
205        Ok(bp.test_count())
206    }
207
208    // Type queries
209
210    pub fn list_type_definitions(&self, bp_id: &BlueprintId) -> ForgeResult<&[TypeDefinition]> {
211        let bp = self.engine.store.load(bp_id)?;
212        Ok(&bp.type_definitions)
213    }
214
215    pub fn get_type_definition(
216        &self,
217        bp_id: &BlueprintId,
218        name: &str,
219    ) -> ForgeResult<&TypeDefinition> {
220        let bp = self.engine.store.load(bp_id)?;
221        bp.type_definitions
222            .iter()
223            .find(|t| t.name == name)
224            .ok_or_else(|| ForgeError::MissingField(name.to_string()))
225    }
226
227    // Function queries
228
229    pub fn list_function_blueprints(
230        &self,
231        bp_id: &BlueprintId,
232    ) -> ForgeResult<&[FunctionBlueprint]> {
233        let bp = self.engine.store.load(bp_id)?;
234        Ok(&bp.function_blueprints)
235    }
236
237    // Architecture queries
238
239    pub fn list_layers(&self, bp_id: &BlueprintId) -> ForgeResult<&[ArchitectureLayer]> {
240        let bp = self.engine.store.load(bp_id)?;
241        Ok(&bp.layers)
242    }
243
244    pub fn list_concerns(&self, bp_id: &BlueprintId) -> ForgeResult<&[CrossCuttingConcern]> {
245        let bp = self.engine.store.load(bp_id)?;
246        Ok(&bp.concerns)
247    }
248
249    // Wiring queries
250
251    pub fn list_wiring(&self, bp_id: &BlueprintId) -> ForgeResult<&[ComponentWiring]> {
252        let bp = self.engine.store.load(bp_id)?;
253        Ok(&bp.wiring)
254    }
255
256    pub fn list_data_flows(&self, bp_id: &BlueprintId) -> ForgeResult<&[DataFlow]> {
257        let bp = self.engine.store.load(bp_id)?;
258        Ok(&bp.data_flows)
259    }
260
261    pub fn list_import_graph(&self, bp_id: &BlueprintId) -> ForgeResult<&[ImportEdge]> {
262        let bp = self.engine.store.load(bp_id)?;
263        Ok(&bp.import_graph)
264    }
265
266    pub fn get_generation_order(&self, bp_id: &BlueprintId) -> ForgeResult<&[String]> {
267        let bp = self.engine.store.load(bp_id)?;
268        Ok(&bp.generation_order)
269    }
270
271    // Validation queries
272
273    pub fn validate_blueprint(&self, bp_id: &BlueprintId) -> ForgeResult<Vec<String>> {
274        let bp = self.engine.store.load(bp_id)?;
275        let mut issues = Vec::new();
276
277        if bp.name.is_empty() {
278            issues.push("Blueprint name is empty".into());
279        }
280        if bp.entities.is_empty() {
281            issues.push("Blueprint has no entities".into());
282        }
283        if bp.files.is_empty() {
284            issues.push("Blueprint has no files".into());
285        }
286
287        for entity in &bp.entities {
288            if entity.fields.is_empty() {
289                issues.push(format!("Entity '{}' has no fields", entity.name));
290            }
291        }
292
293        for file in &bp.files {
294            if file.path.is_empty() {
295                issues.push("File has empty path".into());
296            }
297        }
298
299        Ok(issues)
300    }
301
302    pub fn blueprint_summary(&self, bp_id: &BlueprintId) -> ForgeResult<BlueprintSummary> {
303        let bp = self.engine.store.load(bp_id)?;
304        Ok(BlueprintSummary {
305            id: bp.id,
306            name: bp.name.clone(),
307            domain: bp.domain,
308            status: bp.status,
309            entity_count: bp.entity_count(),
310            file_count: bp.file_count(),
311            dependency_count: bp.dependency_count(),
312            test_count: bp.test_count(),
313            type_count: bp.type_definitions.len(),
314            function_count: bp.function_blueprints.len(),
315        })
316    }
317}
318
319#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
320pub struct BlueprintSummary {
321    pub id: BlueprintId,
322    pub name: String,
323    pub domain: Domain,
324    pub status: BlueprintStatus,
325    pub entity_count: usize,
326    pub file_count: usize,
327    pub dependency_count: usize,
328    pub test_count: usize,
329    pub type_count: usize,
330    pub function_count: usize,
331}
332
333use crate::types::intent::Domain;
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::engine::ForgeEngine;
339    use crate::types::blueprint::*;
340    use crate::types::intent::*;
341
342    fn setup() -> (ForgeEngine, BlueprintId) {
343        let mut engine = ForgeEngine::new();
344        let id = engine
345            .create_blueprint("Test", "Test blueprint", Domain::Api)
346            .unwrap();
347        // Add some entities, files, deps, tests
348        {
349            let mut w = engine.writer();
350            w.add_entity(&id, Entity::new("User", "A user entity"))
351                .unwrap();
352            w.add_entity(&id, Entity::new("Post", "A post entity"))
353                .unwrap();
354            w.add_file(&id, FileBlueprint::new("src/main.rs", FileType::Source))
355                .unwrap();
356            w.add_file(&id, FileBlueprint::new("src/models.rs", FileType::Source))
357                .unwrap();
358            w.add_file(&id, FileBlueprint::new("tests/test.rs", FileType::Test))
359                .unwrap();
360            w.add_dependency(&id, Dependency::new("serde", "1.0"))
361                .unwrap();
362            w.add_dependency(&id, Dependency::new("tokio", "1.35"))
363                .unwrap();
364            w.add_test_case(
365                &id,
366                TestCase::new("test_create", TestType::Unit, "User::create"),
367            )
368            .unwrap();
369            w.add_test_case(&id, TestCase::new("test_e2e", TestType::Integration, "api"))
370                .unwrap();
371        }
372        (engine, id)
373    }
374
375    #[test]
376    fn test_get_blueprint() {
377        let (engine, id) = setup();
378        let r = engine.reader();
379        let bp = r.get_blueprint(&id).unwrap();
380        assert_eq!(bp.name, "Test");
381    }
382
383    #[test]
384    fn test_list_blueprints() {
385        let (engine, _) = setup();
386        let r = engine.reader();
387        assert_eq!(r.list_blueprints().len(), 1);
388    }
389
390    #[test]
391    fn test_search_blueprints() {
392        let (engine, _) = setup();
393        let r = engine.reader();
394        assert_eq!(r.search_blueprints("test").len(), 1);
395        assert_eq!(r.search_blueprints("nonexistent").len(), 0);
396    }
397
398    #[test]
399    fn test_blueprint_count() {
400        let (engine, _) = setup();
401        let r = engine.reader();
402        assert_eq!(r.blueprint_count(), 1);
403    }
404
405    #[test]
406    fn test_get_entity() {
407        let (engine, id) = setup();
408        let r = engine.reader();
409        let entities = r.list_entities(&id).unwrap();
410        let eid = entities[0].id;
411        let entity = r.get_entity(&id, &eid).unwrap();
412        assert!(!entity.name.is_empty());
413    }
414
415    #[test]
416    fn test_get_entity_by_name() {
417        let (engine, id) = setup();
418        let r = engine.reader();
419        let entity = r.get_entity_by_name(&id, "User").unwrap();
420        assert_eq!(entity.name, "User");
421    }
422
423    #[test]
424    fn test_search_entities() {
425        let (engine, id) = setup();
426        let r = engine.reader();
427        let results = r.search_entities(&id, "user").unwrap();
428        assert_eq!(results.len(), 1);
429    }
430
431    #[test]
432    fn test_entity_count() {
433        let (engine, id) = setup();
434        let r = engine.reader();
435        assert_eq!(r.entity_count(&id).unwrap(), 2);
436    }
437
438    #[test]
439    fn test_list_files() {
440        let (engine, id) = setup();
441        let r = engine.reader();
442        assert_eq!(r.list_files(&id).unwrap().len(), 3);
443    }
444
445    #[test]
446    fn test_list_files_by_type() {
447        let (engine, id) = setup();
448        let r = engine.reader();
449        assert_eq!(
450            r.list_files_by_type(&id, FileType::Source).unwrap().len(),
451            2
452        );
453        assert_eq!(r.list_files_by_type(&id, FileType::Test).unwrap().len(), 1);
454    }
455
456    #[test]
457    fn test_file_count() {
458        let (engine, id) = setup();
459        let r = engine.reader();
460        assert_eq!(r.file_count(&id).unwrap(), 3);
461    }
462
463    #[test]
464    fn test_get_file_by_path() {
465        let (engine, id) = setup();
466        let r = engine.reader();
467        let file = r.get_file_by_path(&id, "src/main.rs").unwrap();
468        assert_eq!(file.path, "src/main.rs");
469    }
470
471    #[test]
472    fn test_list_dependencies() {
473        let (engine, id) = setup();
474        let r = engine.reader();
475        assert_eq!(r.list_dependencies(&id).unwrap().len(), 2);
476    }
477
478    #[test]
479    fn test_get_dependency_by_name() {
480        let (engine, id) = setup();
481        let r = engine.reader();
482        let dep = r.get_dependency_by_name(&id, "serde").unwrap();
483        assert_eq!(dep.version, "1.0");
484    }
485
486    #[test]
487    fn test_dependency_count() {
488        let (engine, id) = setup();
489        let r = engine.reader();
490        assert_eq!(r.dependency_count(&id).unwrap(), 2);
491    }
492
493    #[test]
494    fn test_list_test_cases() {
495        let (engine, id) = setup();
496        let r = engine.reader();
497        assert_eq!(r.list_test_cases(&id).unwrap().len(), 2);
498    }
499
500    #[test]
501    fn test_list_tests_by_type() {
502        let (engine, id) = setup();
503        let r = engine.reader();
504        assert_eq!(r.list_tests_by_type(&id, TestType::Unit).unwrap().len(), 1);
505        assert_eq!(
506            r.list_tests_by_type(&id, TestType::Integration)
507                .unwrap()
508                .len(),
509            1
510        );
511    }
512
513    #[test]
514    fn test_validate_blueprint() {
515        let (engine, id) = setup();
516        let r = engine.reader();
517        let issues = r.validate_blueprint(&id).unwrap();
518        assert!(!issues.is_empty());
519    }
520
521    #[test]
522    fn test_blueprint_summary() {
523        let (engine, id) = setup();
524        let r = engine.reader();
525        let summary = r.blueprint_summary(&id).unwrap();
526        assert_eq!(summary.name, "Test");
527        assert_eq!(summary.entity_count, 2);
528        assert_eq!(summary.file_count, 3);
529        assert_eq!(summary.dependency_count, 2);
530        assert_eq!(summary.test_count, 2);
531    }
532
533    #[test]
534    fn test_blueprint_not_found() {
535        let (engine, _) = setup();
536        let fake_id = BlueprintId::new();
537        let r = engine.reader();
538        assert!(r.get_blueprint(&fake_id).is_err());
539    }
540
541    #[test]
542    fn test_entity_not_found() {
543        let (engine, id) = setup();
544        let r = engine.reader();
545        assert!(r.get_entity_by_name(&id, "Nonexistent").is_err());
546    }
547
548    #[test]
549    fn test_list_aggregate_roots() {
550        let (engine, id) = setup();
551        let r = engine.reader();
552        assert_eq!(r.list_aggregate_roots(&id).unwrap().len(), 0);
553    }
554
555    #[test]
556    fn test_get_generation_order() {
557        let (engine, id) = setup();
558        let r = engine.reader();
559        assert!(r.get_generation_order(&id).unwrap().is_empty());
560    }
561
562    #[test]
563    fn test_list_wiring() {
564        let (engine, id) = setup();
565        let r = engine.reader();
566        assert!(r.list_wiring(&id).unwrap().is_empty());
567    }
568
569    #[test]
570    fn test_list_data_flows() {
571        let (engine, id) = setup();
572        let r = engine.reader();
573        assert!(r.list_data_flows(&id).unwrap().is_empty());
574    }
575
576    #[test]
577    fn test_list_import_graph() {
578        let (engine, id) = setup();
579        let r = engine.reader();
580        assert!(r.list_import_graph(&id).unwrap().is_empty());
581    }
582}