Skip to main content

spec_driven_docs/services/
agents_render.rs

1//! Render the managed documentation block for a root `AGENTS.md`.
2//!
3//! The block is the canonical documentation-routing section, wrapped in the
4//! `AGENTS.md` markers with the profile's documentation root substituted and
5//! the project's writing-style selection rendered as one route line, or as
6//! no line where the selection is `none`. The installer places it; this
7//! only produces the bytes. The content lives in the embedded snippet, so
8//! the block and the snippet cannot drift.
9
10use crate::domain::instance_config::WritingStyle;
11use crate::domain::marker::{AGENTS_BEGIN, AGENTS_END};
12
13/// The list item that stands for the route in the snippet. It is a list
14/// item rather than a bare placeholder so a markdown formatter reads the
15/// snippet as the list it is.
16const ROUTE_PLACEHOLDER: &str = "- {writing_style}";
17
18/// The embedded documentation snippet, with `{docs_root}` and
19/// `{writing_style}` unresolved.
20#[must_use]
21pub fn snippet() -> &'static str {
22    crate::embedded::SNIPPETS
23        .get_file("AGENTS-docs.md")
24        .and_then(include_dir::File::contents_utf8)
25        .unwrap_or_default()
26}
27
28/// The one route line the block carries for a selection, or `None` where
29/// the selection installs no route.
30#[must_use]
31pub fn route_line(selection: &WritingStyle) -> Option<String> {
32    selection
33        .route()
34        .map(|route| format!("- Read the writing style before you author or edit prose: {route}."))
35}
36
37/// The complete marked block for the given documentation root and
38/// writing-style selection, newline-terminated.
39#[must_use]
40#[allow(
41    clippy::literal_string_with_formatting_args,
42    reason = "the braces are the block template's placeholder, not a formatting argument"
43)]
44pub fn render_block(docs_root: &str, selection: &WritingStyle) -> String {
45    let mut body = String::new();
46    for line in snippet().replace("{docs_root}", docs_root).lines() {
47        let line = if line == ROUTE_PLACEHOLDER {
48            match route_line(selection) {
49                Some(route) => route,
50                None => continue,
51            }
52        } else {
53            line.to_string()
54        };
55        body.push_str(&line);
56        body.push('\n');
57    }
58    let body = body.trim_end_matches('\n');
59    format!("{AGENTS_BEGIN}\n{body}\n{AGENTS_END}\n")
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::domain::instance_config::WritingSource;
66
67    #[test]
68    fn the_block_carries_the_markers_and_the_root() {
69        let block = render_block("docs", &WritingStyle::default());
70        assert!(block.starts_with("<!-- BEGIN spec-driven-docs docs -->\n"));
71        assert!(block.ends_with("<!-- END spec-driven-docs docs -->\n"));
72        assert!(block.contains(
73            "Read the writing style before you author or edit prose: `sdd method writing-style`."
74        ));
75        assert!(!block.contains("{docs_root}"));
76        assert!(!block.contains("{writing_style}"));
77        assert!(!block.contains("simple-english"));
78    }
79
80    #[test]
81    fn the_underscore_root_reaches_the_block() {
82        let block = render_block("_docs", &WritingStyle::default());
83        assert!(block.contains(
84            "Read the writing style before you author or edit prose: `sdd method writing-style`."
85        ));
86        assert!(block.contains("`_docs/specs/SPEC-<domain>.md`"));
87    }
88
89    #[test]
90    fn the_route_matches_each_selection() {
91        let project = WritingStyle {
92            source: WritingSource::Project,
93            path: Some("docs/STYLE.md".to_string()),
94        };
95        assert!(
96            render_block("docs", &project).contains(
97                "Read the writing style before you author or edit prose: `docs/STYLE.md`."
98            )
99        );
100        let none = WritingStyle {
101            source: WritingSource::None,
102            path: None,
103        };
104        let block = render_block("docs", &none);
105        assert!(!block.contains("writing style"), "{block}");
106        assert!(!block.contains("{writing_style}"));
107        assert!(
108            !block.contains("\n\n<!-- END"),
109            "the removed route left a blank line:\n{block}"
110        );
111    }
112}