Skip to main content

agent_policy/render/
windsurf_rules.rs

1use camino::Utf8PathBuf;
2use minijinja::Environment;
3
4use crate::{
5    error::{Error, Result},
6    model::normalized::Policy,
7    render::RenderedOutput,
8};
9
10const DEFAULT_TARGET: &str = ".windsurf/rules/default.md";
11const DEFAULT_TEMPLATE: &str = include_str!("../../templates/windsurf_default.md.j2");
12const ROLE_TEMPLATE: &str = include_str!("../../templates/windsurf_role.md.j2");
13
14/// Render all `.windsurf/rules/` outputs for the given policy.
15///
16/// # Errors
17///
18/// Returns [`Error::Render`] if any template fails to compile or render.
19pub fn render(policy: &Policy) -> Result<Vec<RenderedOutput>> {
20    let mut outputs = Vec::new();
21
22    let mut env = Environment::new();
23    env.add_template("default.md", DEFAULT_TEMPLATE)
24        .map_err(|e| Error::Render {
25            target: DEFAULT_TARGET.to_owned(),
26            source: e,
27        })?;
28    let tmpl = env.get_template("default.md").map_err(|e| Error::Render {
29        target: DEFAULT_TARGET.to_owned(),
30        source: e,
31    })?;
32    let commands_defined = !policy.commands.is_empty();
33    let content = tmpl
34        .render(minijinja::context! {
35            project => &policy.project,
36            commands => &policy.commands,
37            commands_defined => commands_defined,
38            paths => &policy.paths,
39            roles => &policy.roles,
40            constraints => &policy.constraints,
41        })
42        .map_err(|e| Error::Render {
43            target: DEFAULT_TARGET.to_owned(),
44            source: e,
45        })?;
46    outputs.push(RenderedOutput {
47        path: Utf8PathBuf::from(DEFAULT_TARGET),
48        content,
49    });
50
51    if !policy.roles.is_empty() {
52        let mut role_env = Environment::new();
53        role_env
54            .add_template("role.md", ROLE_TEMPLATE)
55            .map_err(|e| Error::Render {
56                target: "windsurf role".to_owned(),
57                source: e,
58            })?;
59        let role_tmpl = role_env
60            .get_template("role.md")
61            .map_err(|e| Error::Render {
62                target: "windsurf role".to_owned(),
63                source: e,
64            })?;
65
66        for (name, role) in &policy.roles {
67            if role.editable.is_empty() {
68                continue;
69            }
70            let target = format!(".windsurf/rules/{name}.md");
71            let role_content = role_tmpl
72                .render(minijinja::context! {
73                    project => &policy.project,
74                    role_name => name,
75                    role => role,
76                })
77                .map_err(|e| Error::Render {
78                    target: target.clone(),
79                    source: e,
80                })?;
81            outputs.push(RenderedOutput {
82                path: Utf8PathBuf::from(&target),
83                content: role_content,
84            });
85        }
86    }
87
88    Ok(outputs)
89}