Skip to main content

driven/emitter/
claude.rs

1//! Claude Code emitter (.claude/, CLAUDE.md)
2
3use super::{RuleEmitter, ensure_parent_dir, format_bullet_list, format_heading};
4use crate::{Editor, Result, parser::UnifiedRule};
5use std::path::Path;
6
7/// Emitter for Claude Code format
8#[derive(Debug, Default)]
9pub struct ClaudeEmitter;
10
11impl ClaudeEmitter {
12    /// Create a new Claude emitter
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl RuleEmitter for ClaudeEmitter {
19    fn emit_file(&self, rules: &[UnifiedRule], path: &Path) -> Result<()> {
20        ensure_parent_dir(path)?;
21        let content = self.emit_string(rules)?;
22        std::fs::write(path, content)?;
23        Ok(())
24    }
25
26    fn emit_string(&self, rules: &[UnifiedRule]) -> Result<String> {
27        let mut output = String::new();
28
29        output.push_str(&format_heading(1, "CLAUDE.md"));
30        output.push_str("Instructions for Claude Code assistant.\n\n");
31
32        let mut instructions: Vec<String> = Vec::new();
33        let mut context: Vec<String> = Vec::new();
34
35        for rule in rules {
36            match rule {
37                UnifiedRule::Persona {
38                    role, principles, ..
39                } => {
40                    output.push_str(&format_heading(2, "Role"));
41                    output.push_str(role);
42                    output.push_str("\n\n");
43
44                    if !principles.is_empty() {
45                        instructions.extend(principles.clone());
46                    }
47                }
48                UnifiedRule::Standard { description, .. } => {
49                    instructions.push(description.clone());
50                }
51                UnifiedRule::Context {
52                    focus, includes, ..
53                } => {
54                    context.extend(focus.clone());
55                    context.extend(includes.clone());
56                }
57                UnifiedRule::Workflow { name, steps } => {
58                    output.push_str(&format_heading(2, &format!("Workflow: {}", name)));
59                    for (i, step) in steps.iter().enumerate() {
60                        output.push_str(&format!(
61                            "{}. **{}**: {}\n",
62                            i + 1,
63                            step.name,
64                            step.description
65                        ));
66                    }
67                    output.push('\n');
68                }
69                UnifiedRule::Raw { content } => {
70                    output.push_str(content);
71                    output.push_str("\n\n");
72                }
73            }
74        }
75
76        if !context.is_empty() {
77            output.push_str(&format_heading(2, "About This Project"));
78            output.push_str(&format_bullet_list(&context));
79            output.push('\n');
80        }
81
82        if !instructions.is_empty() {
83            output.push_str(&format_heading(2, "Instructions"));
84            output.push_str(&format_bullet_list(&instructions));
85            output.push('\n');
86        }
87
88        Ok(output.trim().to_string())
89    }
90
91    fn editor(&self) -> Editor {
92        Editor::Claude
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_emit_claude() {
102        let rules = vec![UnifiedRule::Standard {
103            category: crate::format::RuleCategory::Style,
104            priority: 0,
105            description: "Always explain your reasoning".to_string(),
106            pattern: None,
107        }];
108
109        let emitter = ClaudeEmitter::new();
110        let output = emitter.emit_string(&rules).unwrap();
111
112        assert!(output.contains("CLAUDE.md"));
113        assert!(output.contains("explain your reasoning"));
114    }
115}