Skip to main content

driven/generator_integration/
mod.rs

1//! Generator Integration Module
2//!
3//! This module provides integration between Driven and the dx-generator crate,
4//! enabling template-based rule generation and spec scaffolding.
5//!
6//! ## Features
7//!
8//! - Template-based rule generation using dx-generator
9//! - Spec scaffolding with generator templates
10//! - Driven templates available in generator registry
11//!
12//! ## Usage
13//!
14//! ```rust,ignore
15//! use driven::generator_integration::{GeneratorBridge, DrivenTemplateProvider};
16//!
17//! // Create a generator bridge
18//! let bridge = GeneratorBridge::new()?;
19//!
20//! // Generate rules from a template
21//! let rules = bridge.generate_rules("rust-workspace", &params)?;
22//!
23//! // Generate spec scaffolding
24//! let files = bridge.generate_spec_scaffold("feature-001", &params)?;
25//! ```
26
27use crate::{DrivenError, Result, UnifiedRule};
28use std::collections::HashMap;
29use std::path::{Path, PathBuf};
30
31/// Bridge between Driven and dx-generator
32///
33/// Provides high-level APIs for using generator templates within Driven workflows.
34#[derive(Debug)]
35pub struct GeneratorBridge {
36    /// Template search paths
37    template_paths: Vec<PathBuf>,
38    /// Cached template metadata
39    template_cache: HashMap<String, TemplateInfo>,
40    /// Whether the bridge is initialized
41    initialized: bool,
42}
43
44/// Information about a template
45#[derive(Debug, Clone)]
46pub struct TemplateInfo {
47    /// Template ID
48    pub id: String,
49    /// Template name
50    pub name: String,
51    /// Template description
52    pub description: String,
53    /// Template category
54    pub category: TemplateCategory,
55    /// Required parameters
56    pub parameters: Vec<ParameterInfo>,
57    /// Output file pattern
58    pub output_pattern: String,
59}
60
61/// Template categories
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum TemplateCategory {
64    /// Rule templates for AI editors
65    Rule,
66    /// Spec templates for spec-driven development
67    Spec,
68    /// Scaffold templates for project structure
69    Scaffold,
70    /// Component templates
71    Component,
72    /// Other templates
73    Other,
74}
75
76/// Parameter information
77#[derive(Debug, Clone)]
78pub struct ParameterInfo {
79    /// Parameter name
80    pub name: String,
81    /// Parameter description
82    pub description: String,
83    /// Whether the parameter is required
84    pub required: bool,
85    /// Default value if any
86    pub default: Option<String>,
87}
88
89/// Generated file from a template
90#[derive(Debug, Clone)]
91pub struct GeneratedFile {
92    /// Relative path for the file
93    pub path: PathBuf,
94    /// File content
95    pub content: String,
96    /// Whether this file should overwrite existing
97    pub overwrite: bool,
98}
99
100/// Parameters for template generation
101#[derive(Debug, Clone, Default)]
102pub struct GenerateParams {
103    /// Key-value parameters
104    params: HashMap<String, ParamValue>,
105}
106
107/// Parameter value types
108#[derive(Debug, Clone)]
109pub enum ParamValue {
110    /// String value
111    String(String),
112    /// Integer value
113    Integer(i64),
114    /// Boolean value
115    Boolean(bool),
116    /// Array of strings
117    Array(Vec<String>),
118}
119
120impl GenerateParams {
121    /// Create new empty parameters
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Set a string parameter
127    pub fn set_string(mut self, key: &str, value: impl Into<String>) -> Self {
128        self.params
129            .insert(key.to_string(), ParamValue::String(value.into()));
130        self
131    }
132
133    /// Set an integer parameter
134    pub fn set_int(mut self, key: &str, value: i64) -> Self {
135        self.params
136            .insert(key.to_string(), ParamValue::Integer(value));
137        self
138    }
139
140    /// Set a boolean parameter
141    pub fn set_bool(mut self, key: &str, value: bool) -> Self {
142        self.params
143            .insert(key.to_string(), ParamValue::Boolean(value));
144        self
145    }
146
147    /// Set an array parameter
148    pub fn set_array(mut self, key: &str, value: Vec<String>) -> Self {
149        self.params
150            .insert(key.to_string(), ParamValue::Array(value));
151        self
152    }
153
154    /// Get a parameter value
155    pub fn get(&self, key: &str) -> Option<&ParamValue> {
156        self.params.get(key)
157    }
158
159    /// Get a string parameter
160    pub fn get_string(&self, key: &str) -> Option<&str> {
161        match self.params.get(key) {
162            Some(ParamValue::String(s)) => Some(s),
163            _ => None,
164        }
165    }
166
167    /// Get an integer parameter
168    pub fn get_int(&self, key: &str) -> Option<i64> {
169        match self.params.get(key) {
170            Some(ParamValue::Integer(i)) => Some(*i),
171            _ => None,
172        }
173    }
174
175    /// Get a boolean parameter
176    pub fn get_bool(&self, key: &str) -> Option<bool> {
177        match self.params.get(key) {
178            Some(ParamValue::Boolean(b)) => Some(*b),
179            _ => None,
180        }
181    }
182
183    /// Convert to a HashMap of strings for template rendering
184    pub fn to_string_map(&self) -> HashMap<String, String> {
185        self.params
186            .iter()
187            .map(|(k, v)| {
188                let value = match v {
189                    ParamValue::String(s) => s.clone(),
190                    ParamValue::Integer(i) => i.to_string(),
191                    ParamValue::Boolean(b) => b.to_string(),
192                    ParamValue::Array(a) => a.join(", "),
193                };
194                (k.clone(), value)
195            })
196            .collect()
197    }
198}
199
200impl GeneratorBridge {
201    /// Create a new generator bridge with default template paths
202    pub fn new() -> Result<Self> {
203        let template_paths = vec![
204            PathBuf::from(".driven/templates"),
205            PathBuf::from(".dx/templates"),
206        ];
207
208        Ok(Self {
209            template_paths,
210            template_cache: HashMap::new(),
211            initialized: false,
212        })
213    }
214
215    /// Create a generator bridge with custom template paths
216    pub fn with_paths(paths: Vec<PathBuf>) -> Result<Self> {
217        Ok(Self {
218            template_paths: paths,
219            template_cache: HashMap::new(),
220            initialized: false,
221        })
222    }
223
224    /// Add a template search path
225    pub fn add_path(&mut self, path: impl AsRef<Path>) {
226        self.template_paths.push(path.as_ref().to_path_buf());
227    }
228
229    /// Initialize the bridge by scanning for templates
230    pub fn initialize(&mut self) -> Result<()> {
231        self.template_cache.clear();
232
233        // Clone paths to avoid borrow issues
234        let paths: Vec<PathBuf> = self.template_paths.clone();
235
236        for path in &paths {
237            if path.exists() {
238                self.scan_templates(path)?;
239            }
240        }
241
242        self.initialized = true;
243        Ok(())
244    }
245
246    /// Scan a directory for templates
247    fn scan_templates(&mut self, dir: &Path) -> Result<()> {
248        if !dir.is_dir() {
249            return Ok(());
250        }
251
252        for entry in std::fs::read_dir(dir).map_err(DrivenError::Io)? {
253            let entry = entry.map_err(DrivenError::Io)?;
254            let path = entry.path();
255
256            if path.is_file() {
257                if let Some(ext) = path.extension() {
258                    if ext == "dxt" || ext == "hbs" || ext == "template" {
259                        if let Ok(info) = self.parse_template_info(&path) {
260                            self.template_cache.insert(info.id.clone(), info);
261                        }
262                    }
263                }
264            } else if path.is_dir() {
265                // Recursively scan subdirectories
266                self.scan_templates(&path)?;
267            }
268        }
269
270        Ok(())
271    }
272
273    /// Parse template information from a file
274    fn parse_template_info(&self, path: &Path) -> Result<TemplateInfo> {
275        let id = path
276            .file_stem()
277            .and_then(|s| s.to_str())
278            .ok_or_else(|| DrivenError::Template("Invalid template filename".to_string()))?
279            .to_string();
280
281        // For now, create basic info from filename
282        // In a full implementation, this would parse template metadata
283        Ok(TemplateInfo {
284            id: id.clone(),
285            name: id.replace(['-', '_'], " "),
286            description: format!("Template from {}", path.display()),
287            category: self.infer_category(&id),
288            parameters: Vec::new(),
289            output_pattern: String::new(),
290        })
291    }
292
293    /// Infer template category from ID
294    fn infer_category(&self, id: &str) -> TemplateCategory {
295        if id.contains("rule") || id.contains("cursor") || id.contains("copilot") {
296            TemplateCategory::Rule
297        } else if id.contains("spec") || id.contains("requirement") || id.contains("design") {
298            TemplateCategory::Spec
299        } else if id.contains("scaffold") || id.contains("project") {
300            TemplateCategory::Scaffold
301        } else if id.contains("component") || id.contains("model") {
302            TemplateCategory::Component
303        } else {
304            TemplateCategory::Other
305        }
306    }
307
308    /// List available templates
309    pub fn list_templates(&self) -> Vec<&TemplateInfo> {
310        self.template_cache.values().collect()
311    }
312
313    /// List templates by category
314    pub fn list_templates_by_category(&self, category: TemplateCategory) -> Vec<&TemplateInfo> {
315        self.template_cache
316            .values()
317            .filter(|t| t.category == category)
318            .collect()
319    }
320
321    /// Get a template by ID
322    pub fn get_template(&self, id: &str) -> Option<&TemplateInfo> {
323        self.template_cache.get(id)
324    }
325
326    /// Generate rules from a template
327    ///
328    /// Uses the generator to render a rule template and parse the result
329    /// into unified rules.
330    pub fn generate_rules(
331        &self,
332        template_id: &str,
333        params: &GenerateParams,
334    ) -> Result<Vec<UnifiedRule>> {
335        let _template = self
336            .get_template(template_id)
337            .ok_or_else(|| DrivenError::TemplateNotFound(template_id.to_string()))?;
338
339        // In a full implementation, this would:
340        // 1. Load the template using dx-generator
341        // 2. Render with the provided parameters
342        // 3. Parse the output as rules
343
344        // For now, return a placeholder
345        let _params_map = params.to_string_map();
346
347        Ok(Vec::new())
348    }
349
350    /// Generate spec scaffolding
351    ///
352    /// Creates the directory structure and initial files for a new spec.
353    pub fn generate_spec_scaffold(
354        &self,
355        spec_id: &str,
356        params: &GenerateParams,
357    ) -> Result<Vec<GeneratedFile>> {
358        let mut files = Vec::new();
359
360        // Get spec name from params or use ID
361        let spec_name = params.get_string("name").unwrap_or(spec_id);
362
363        // Generate requirements.md
364        files.push(GeneratedFile {
365            path: PathBuf::from(format!(".driven/specs/{}/requirements.md", spec_id)),
366            content: self.generate_requirements_template(spec_name, params),
367            overwrite: false,
368        });
369
370        // Generate design.md
371        files.push(GeneratedFile {
372            path: PathBuf::from(format!(".driven/specs/{}/design.md", spec_id)),
373            content: self.generate_design_template(spec_name, params),
374            overwrite: false,
375        });
376
377        // Generate tasks.md
378        files.push(GeneratedFile {
379            path: PathBuf::from(format!(".driven/specs/{}/tasks.md", spec_id)),
380            content: self.generate_tasks_template(spec_name, params),
381            overwrite: false,
382        });
383
384        Ok(files)
385    }
386
387    /// Generate requirements template content
388    fn generate_requirements_template(&self, name: &str, _params: &GenerateParams) -> String {
389        format!(
390            r#"# Requirements Document
391
392## Introduction
393
394{name}
395
396## Glossary
397
398- **System**: [Definition]
399
400## Requirements
401
402### Requirement 1
403
404**User Story:** As a [role], I want [feature], so that [benefit]
405
406#### Acceptance Criteria
407
4081. WHEN [event], THE [System] SHALL [response]
409"#,
410            name = name
411        )
412    }
413
414    /// Generate design template content
415    fn generate_design_template(&self, name: &str, _params: &GenerateParams) -> String {
416        format!(
417            r#"# Design Document: {name}
418
419## Overview
420
421[Summary of the design]
422
423## Architecture
424
425[Architecture description]
426
427## Components and Interfaces
428
429[Component descriptions]
430
431## Data Models
432
433[Data model definitions]
434
435## Correctness Properties
436
437[Properties to validate]
438
439## Error Handling
440
441[Error handling strategy]
442
443## Testing Strategy
444
445[Testing approach]
446"#,
447            name = name
448        )
449    }
450
451    /// Generate tasks template content
452    fn generate_tasks_template(&self, name: &str, _params: &GenerateParams) -> String {
453        format!(
454            r#"# Implementation Plan: {name}
455
456## Overview
457
458[Implementation approach]
459
460## Tasks
461
462- [ ] 1. Set up project structure
463  - [ ] 1.1 Create directory structure
464  - [ ] 1.2 Define core interfaces
465  - _Requirements: 1.1_
466
467- [ ] 2. Implement core functionality
468  - [ ] 2.1 Implement main logic
469  - _Requirements: 1.2_
470
471- [ ] 3. Checkpoint - Ensure all tests pass
472  - Ensure all tests pass, ask the user if questions arise.
473
474## Notes
475
476- Tasks marked with `*` are optional
477- Each task references specific requirements for traceability
478"#,
479            name = name
480        )
481    }
482
483    /// Write generated files to disk
484    pub fn write_files(&self, files: &[GeneratedFile]) -> Result<Vec<PathBuf>> {
485        let mut written = Vec::new();
486
487        for file in files {
488            // Create parent directories
489            if let Some(parent) = file.path.parent() {
490                std::fs::create_dir_all(parent).map_err(DrivenError::Io)?;
491            }
492
493            // Check if file exists and overwrite flag
494            if file.path.exists() && !file.overwrite {
495                continue;
496            }
497
498            // Write the file
499            std::fs::write(&file.path, &file.content).map_err(DrivenError::Io)?;
500
501            written.push(file.path.clone());
502        }
503
504        Ok(written)
505    }
506}
507
508impl Default for GeneratorBridge {
509    fn default() -> Self {
510        Self::new().unwrap_or_else(|_| Self {
511            template_paths: Vec::new(),
512            template_cache: HashMap::new(),
513            initialized: false,
514        })
515    }
516}
517
518/// Driven template provider for dx-generator
519///
520/// Makes Driven templates available in the generator registry.
521#[derive(Debug, Default)]
522pub struct DrivenTemplateProvider {
523    /// Registered templates
524    templates: HashMap<String, DrivenTemplate>,
525}
526
527/// A Driven-specific template
528#[derive(Debug, Clone)]
529pub struct DrivenTemplate {
530    /// Template ID
531    pub id: String,
532    /// Template content
533    pub content: String,
534    /// Template type
535    pub template_type: DrivenTemplateType,
536}
537
538/// Types of Driven templates
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum DrivenTemplateType {
541    /// Rule template for AI editors
542    Rule,
543    /// Spec template
544    Spec,
545    /// Hook template
546    Hook,
547    /// Steering template
548    Steering,
549}
550
551impl DrivenTemplateProvider {
552    /// Create a new template provider
553    pub fn new() -> Self {
554        Self::default()
555    }
556
557    /// Register a template
558    pub fn register(&mut self, template: DrivenTemplate) {
559        self.templates.insert(template.id.clone(), template);
560    }
561
562    /// Get a template by ID
563    pub fn get(&self, id: &str) -> Option<&DrivenTemplate> {
564        self.templates.get(id)
565    }
566
567    /// List all templates
568    pub fn list(&self) -> Vec<&DrivenTemplate> {
569        self.templates.values().collect()
570    }
571
572    /// Register built-in templates
573    pub fn register_builtins(&mut self) {
574        // Register rule templates
575        self.register(DrivenTemplate {
576            id: "driven-rule-basic".to_string(),
577            content: include_str!("templates/rule-basic.md").to_string(),
578            template_type: DrivenTemplateType::Rule,
579        });
580
581        // Register spec templates
582        self.register(DrivenTemplate {
583            id: "driven-spec-requirements".to_string(),
584            content: include_str!("templates/spec-requirements.md").to_string(),
585            template_type: DrivenTemplateType::Spec,
586        });
587
588        // Register hook templates
589        self.register(DrivenTemplate {
590            id: "driven-hook-file-save".to_string(),
591            content: include_str!("templates/hook-file-save.toml").to_string(),
592            template_type: DrivenTemplateType::Hook,
593        });
594
595        // Register steering templates
596        self.register(DrivenTemplate {
597            id: "driven-steering-always".to_string(),
598            content: include_str!("templates/steering-always.md").to_string(),
599            template_type: DrivenTemplateType::Steering,
600        });
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn test_generate_params() {
610        let params = GenerateParams::new()
611            .set_string("name", "TestFeature")
612            .set_int("version", 1)
613            .set_bool("enabled", true);
614
615        assert_eq!(params.get_string("name"), Some("TestFeature"));
616        assert_eq!(params.get_int("version"), Some(1));
617        assert_eq!(params.get_bool("enabled"), Some(true));
618    }
619
620    #[test]
621    fn test_params_to_string_map() {
622        let params = GenerateParams::new()
623            .set_string("name", "Test")
624            .set_int("count", 42)
625            .set_bool("flag", true);
626
627        let map = params.to_string_map();
628        assert_eq!(map.get("name"), Some(&"Test".to_string()));
629        assert_eq!(map.get("count"), Some(&"42".to_string()));
630        assert_eq!(map.get("flag"), Some(&"true".to_string()));
631    }
632
633    #[test]
634    fn test_generator_bridge_creation() {
635        let bridge = GeneratorBridge::new();
636        assert!(bridge.is_ok());
637    }
638
639    #[test]
640    fn test_infer_category() {
641        let bridge = GeneratorBridge::new().unwrap();
642
643        assert_eq!(
644            bridge.infer_category("cursor-rules"),
645            TemplateCategory::Rule
646        );
647        assert_eq!(
648            bridge.infer_category("spec-requirements"),
649            TemplateCategory::Spec
650        );
651        assert_eq!(
652            bridge.infer_category("project-scaffold"),
653            TemplateCategory::Scaffold
654        );
655        assert_eq!(
656            bridge.infer_category("react-component"),
657            TemplateCategory::Component
658        );
659        assert_eq!(
660            bridge.infer_category("other-template"),
661            TemplateCategory::Other
662        );
663    }
664
665    #[test]
666    fn test_spec_scaffold_generation() {
667        let bridge = GeneratorBridge::new().unwrap();
668        let params = GenerateParams::new().set_string("name", "Test Feature");
669
670        let files = bridge.generate_spec_scaffold("001", &params).unwrap();
671
672        assert_eq!(files.len(), 3);
673        assert!(
674            files
675                .iter()
676                .any(|f| f.path.to_string_lossy().contains("requirements.md"))
677        );
678        assert!(
679            files
680                .iter()
681                .any(|f| f.path.to_string_lossy().contains("design.md"))
682        );
683        assert!(
684            files
685                .iter()
686                .any(|f| f.path.to_string_lossy().contains("tasks.md"))
687        );
688    }
689
690    #[test]
691    fn test_driven_template_provider() {
692        let mut provider = DrivenTemplateProvider::new();
693
694        provider.register(DrivenTemplate {
695            id: "test-template".to_string(),
696            content: "Test content".to_string(),
697            template_type: DrivenTemplateType::Rule,
698        });
699
700        assert!(provider.get("test-template").is_some());
701        assert_eq!(provider.list().len(), 1);
702    }
703}