Skip to main content

agent_policy/model/
targets.rs

1// Output target flags — which compatibility files to generate.
2
3use serde::Serialize;
4
5/// A stable identifier for each supported output target.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
7#[serde(rename_all = "kebab-case")]
8pub enum TargetId {
9    AgentsMd,
10    ClaudeMd,
11    CursorRules,
12    GeminiMd,
13    CopilotInstructions,
14    Clinerules,
15    WindsurfRules,
16    CopilotInstructionsScoped,
17    JunieGuidelines,
18}
19
20impl TargetId {
21    /// All targets in a defined stable order.
22    pub const ALL: &'static [TargetId] = &[
23        TargetId::AgentsMd,
24        TargetId::ClaudeMd,
25        TargetId::CursorRules,
26        TargetId::GeminiMd,
27        TargetId::CopilotInstructions,
28        TargetId::Clinerules,
29        TargetId::WindsurfRules,
30        TargetId::CopilotInstructionsScoped,
31        TargetId::JunieGuidelines,
32    ];
33
34    /// The YAML ID string used in `outputs:` lists.
35    #[must_use]
36    pub fn id(self) -> &'static str {
37        match self {
38            TargetId::AgentsMd => "agents-md",
39            TargetId::ClaudeMd => "claude-md",
40            TargetId::CursorRules => "cursor-rules",
41            TargetId::GeminiMd => "gemini-md",
42            TargetId::CopilotInstructions => "copilot-instructions",
43            TargetId::Clinerules => "clinerules",
44            TargetId::WindsurfRules => "windsurf-rules",
45            TargetId::CopilotInstructionsScoped => "copilot-instructions-scoped",
46            TargetId::JunieGuidelines => "junie-guidelines",
47        }
48    }
49
50    /// Parse a target ID from its YAML string representation.
51    #[must_use]
52    pub fn from_id(id: &str) -> Option<Self> {
53        match id {
54            "agents-md" => Some(TargetId::AgentsMd),
55            "claude-md" => Some(TargetId::ClaudeMd),
56            "cursor-rules" => Some(TargetId::CursorRules),
57            "gemini-md" => Some(TargetId::GeminiMd),
58            "copilot-instructions" => Some(TargetId::CopilotInstructions),
59            "clinerules" => Some(TargetId::Clinerules),
60            "windsurf-rules" => Some(TargetId::WindsurfRules),
61            "copilot-instructions-scoped" => Some(TargetId::CopilotInstructionsScoped),
62            "junie-guidelines" => Some(TargetId::JunieGuidelines),
63            _ => None,
64        }
65    }
66
67    /// A human-readable display label.
68    #[must_use]
69    pub fn label(self) -> &'static str {
70        match self {
71            TargetId::AgentsMd => "AGENTS.md",
72            TargetId::ClaudeMd => "CLAUDE.md",
73            TargetId::CursorRules => ".cursor/rules/",
74            TargetId::GeminiMd => "GEMINI.md",
75            TargetId::CopilotInstructions => ".github/copilot-instructions.md",
76            TargetId::Clinerules => ".clinerules/",
77            TargetId::WindsurfRules => ".windsurf/rules/",
78            TargetId::CopilotInstructionsScoped => ".github/instructions/",
79            TargetId::JunieGuidelines => ".junie/guidelines.md",
80        }
81    }
82
83    /// Primary output path produced by this target.
84    #[must_use]
85    pub fn primary_path(self) -> &'static str {
86        match self {
87            TargetId::AgentsMd => "AGENTS.md",
88            TargetId::ClaudeMd => "CLAUDE.md",
89            TargetId::CursorRules => ".cursor/rules/default.mdc",
90            TargetId::GeminiMd => "GEMINI.md",
91            TargetId::CopilotInstructions => ".github/copilot-instructions.md",
92            TargetId::Clinerules => ".clinerules/default.md",
93            TargetId::WindsurfRules => ".windsurf/rules/default.md",
94            TargetId::CopilotInstructionsScoped => ".github/instructions/default.md",
95            TargetId::JunieGuidelines => ".junie/guidelines.md",
96        }
97    }
98
99    /// Glob pattern(s) that cover all output files produced by this target.
100    ///
101    /// Used to auto-populate `paths.generated` in the normalized model so that
102    /// users do not need to list output files in both `outputs:` and
103    /// `paths.generated:`.
104    #[must_use]
105    pub fn generated_glob(self) -> &'static str {
106        match self {
107            TargetId::AgentsMd => "AGENTS.md",
108            TargetId::ClaudeMd => "CLAUDE.md",
109            TargetId::CursorRules => ".cursor/rules/**",
110            TargetId::GeminiMd => "GEMINI.md",
111            TargetId::CopilotInstructions => ".github/copilot-instructions.md",
112            TargetId::Clinerules => ".clinerules/**",
113            TargetId::WindsurfRules => ".windsurf/rules/**",
114            TargetId::CopilotInstructionsScoped => ".github/instructions/**",
115            TargetId::JunieGuidelines => ".junie/guidelines.md",
116        }
117    }
118
119    /// Support tier: `"stable"` or `"experimental"`.
120    #[must_use]
121    pub fn tier(self) -> Tier {
122        match self {
123            TargetId::AgentsMd
124            | TargetId::ClaudeMd
125            | TargetId::CursorRules
126            | TargetId::GeminiMd
127            | TargetId::CopilotInstructions => Tier::Stable,
128            TargetId::Clinerules
129            | TargetId::WindsurfRules
130            | TargetId::CopilotInstructionsScoped
131            | TargetId::JunieGuidelines => Tier::Experimental,
132        }
133    }
134}
135
136/// The stability tier of a target.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
138#[serde(rename_all = "kebab-case")]
139pub enum Tier {
140    Stable,
141    Experimental,
142}
143
144impl Tier {
145    /// String representation of the tier.
146    #[must_use]
147    pub fn as_str(self) -> &'static str {
148        match self {
149            Tier::Stable => "stable",
150            Tier::Experimental => "experimental",
151        }
152    }
153}
154
155/// Which output files the policy is configured to generate.
156#[allow(clippy::struct_excessive_bools)]
157#[derive(Debug, Clone, Serialize)]
158pub struct OutputTargets {
159    /// Generate `AGENTS.md` (default: true when `outputs` is omitted).
160    pub agents_md: bool,
161    /// Generate `CLAUDE.md`.
162    pub claude_md: bool,
163    /// Generate `.cursor/rules/default.mdc`.
164    pub cursor_rules: bool,
165    /// Generate `GEMINI.md`.
166    pub gemini_md: bool,
167    /// Generate `.github/copilot-instructions.md`.
168    pub copilot_instructions: bool,
169    /// Generate `.clinerules/` directory.
170    pub clinerules: bool,
171    /// Generate `.windsurf/rules/` directory.
172    pub windsurf_rules: bool,
173    /// Generate `.github/instructions/` directory.
174    pub copilot_instructions_scoped: bool,
175    /// Generate `.junie/guidelines.md`.
176    pub junie_guidelines: bool,
177}
178
179impl Default for OutputTargets {
180    fn default() -> Self {
181        Self {
182            agents_md: true,
183            claude_md: false,
184            cursor_rules: false,
185            gemini_md: false,
186            copilot_instructions: false,
187            clinerules: false,
188            windsurf_rules: false,
189            copilot_instructions_scoped: false,
190            junie_guidelines: false,
191        }
192    }
193}
194
195impl OutputTargets {
196    /// Returns `true` if no outputs are enabled.
197    #[must_use]
198    pub fn is_empty(&self) -> bool {
199        !self.agents_md
200            && !self.claude_md
201            && !self.cursor_rules
202            && !self.gemini_md
203            && !self.copilot_instructions
204            && !self.clinerules
205            && !self.windsurf_rules
206            && !self.copilot_instructions_scoped
207            && !self.junie_guidelines
208    }
209
210    /// Returns the list of enabled [`TargetId`]s in stable order.
211    #[must_use]
212    pub fn enabled(&self) -> Vec<TargetId> {
213        let mut out = Vec::new();
214        if self.agents_md {
215            out.push(TargetId::AgentsMd);
216        }
217        if self.claude_md {
218            out.push(TargetId::ClaudeMd);
219        }
220        if self.cursor_rules {
221            out.push(TargetId::CursorRules);
222        }
223        if self.gemini_md {
224            out.push(TargetId::GeminiMd);
225        }
226        if self.copilot_instructions {
227            out.push(TargetId::CopilotInstructions);
228        }
229        if self.clinerules {
230            out.push(TargetId::Clinerules);
231        }
232        if self.windsurf_rules {
233            out.push(TargetId::WindsurfRules);
234        }
235        if self.copilot_instructions_scoped {
236            out.push(TargetId::CopilotInstructionsScoped);
237        }
238        if self.junie_guidelines {
239            out.push(TargetId::JunieGuidelines);
240        }
241        out
242    }
243
244    /// Construct `OutputTargets` directly from a list of `TargetId`s.
245    #[must_use]
246    pub fn from_targets(targets: &[TargetId]) -> Self {
247        let mut out = Self {
248            agents_md: false,
249            claude_md: false,
250            cursor_rules: false,
251            gemini_md: false,
252            copilot_instructions: false,
253            clinerules: false,
254            windsurf_rules: false,
255            copilot_instructions_scoped: false,
256            junie_guidelines: false,
257        };
258        for t in targets {
259            match t {
260                TargetId::AgentsMd => out.agents_md = true,
261                TargetId::ClaudeMd => out.claude_md = true,
262                TargetId::CursorRules => out.cursor_rules = true,
263                TargetId::GeminiMd => out.gemini_md = true,
264                TargetId::CopilotInstructions => out.copilot_instructions = true,
265                TargetId::Clinerules => out.clinerules = true,
266                TargetId::WindsurfRules => out.windsurf_rules = true,
267                TargetId::CopilotInstructionsScoped => out.copilot_instructions_scoped = true,
268                TargetId::JunieGuidelines => out.junie_guidelines = true,
269            }
270        }
271        out
272    }
273}