Skip to main content

spec_driven_docs/
embedded.rs

1//! Compile-time embedded assets: the payload and the method, in the binary.
2//!
3//! Every `include_dir!`/`include_str!` in the crate lives here, and each
4//! embeds from its canonical authored path, so the repository file and the
5//! shipped copy cannot diverge — the build reads the real thing. This module
6//! only holds bytes and typed accessors; deciding where an asset lands in an
7//! instance is the profiles' and installer's business.
8
9use std::collections::BTreeSet;
10
11use include_dir::{Dir, include_dir};
12
13/// The spec seeds an instance adopts, and the canon-only specs beside them.
14pub static SPECS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/_docs/specs");
15/// The stable document templates.
16pub static TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates");
17/// The markdownlint configurations the instance receives managed.
18pub static MARKDOWNLINT: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/.markdownlint");
19/// Integration snippets a consumer copies into their own files.
20pub static SNIPPETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/instance/snippets");
21/// The method chapters and glossary.
22pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
23/// The cross-agent skills, one `SKILL.md` per directory.
24pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
25
26/// The combined license statement naming both halves.
27pub static LICENSE: &str = include_str!("../LICENSE");
28/// The MIT license covering the distribution.
29pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
30/// The CC BY 4.0 license covering the method.
31pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
32
33const SOURCE_ROOTS: &[(&str, &Dir<'static>)] = &[
34    ("_docs/specs/", &SPECS),
35    ("templates/", &TEMPLATES),
36    (".markdownlint/", &MARKDOWNLINT),
37    ("instance/snippets/", &SNIPPETS),
38    ("skills/", &SKILLS),
39];
40
41/// Every skill name, sorted; a name is the skill's directory.
42#[must_use]
43pub fn skill_names() -> Vec<&'static str> {
44    let mut names: Vec<&'static str> = SKILLS
45        .dirs()
46        .filter_map(|dir| dir.path().as_os_str().to_str())
47        .collect();
48    names.sort_unstable();
49    names
50}
51
52/// One skill's `SKILL.md` text, by skill name.
53#[must_use]
54pub fn skill(name: &str) -> Option<&'static str> {
55    SKILLS
56        .get_file(format!("{name}/SKILL.md"))
57        .and_then(include_dir::File::contents_utf8)
58}
59
60/// Resolve a payload source path — as a profile projection names it — to its
61/// embedded bytes.
62#[must_use]
63pub fn asset(source: &str) -> Option<&'static [u8]> {
64    SOURCE_ROOTS.iter().find_map(|(prefix, dir)| {
65        let rest = source.strip_prefix(prefix)?;
66        dir.get_file(rest).map(include_dir::File::contents)
67    })
68}
69
70/// Every `` ### `domain:rule` `` requirement address the embedded specs define.
71#[must_use]
72pub fn spec_rule_ids() -> BTreeSet<String> {
73    let mut ids = BTreeSet::new();
74    for file in SPECS.files() {
75        let Some(text) = file.contents_utf8() else {
76            continue;
77        };
78        ids.extend(rule_ids_in(text));
79    }
80    ids
81}
82
83/// The requirement addresses one spec document defines.
84pub fn rule_ids_in(text: &str) -> impl Iterator<Item = String> + '_ {
85    text.lines().filter_map(|line| {
86        let candidate = line.strip_prefix("### `")?;
87        let (id, _) = candidate.split_once('`')?;
88        let (domain, rule) = id.split_once(':')?;
89        let is_slug = |part: &str| {
90            !part.is_empty()
91                && part
92                    .bytes()
93                    .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
94        };
95        (is_slug(domain) && is_slug(rule)).then(|| id.to_string())
96    })
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::domain::profile::ProfileId;
103    use crate::domain::rule_id::RuleId;
104
105    #[test]
106    fn rule_id_enum_matches_the_embedded_specs() {
107        let from_specs = spec_rule_ids();
108        let from_enum: BTreeSet<String> = RuleId::ALL
109            .iter()
110            .map(|rule| rule.as_str().to_string())
111            .collect();
112        assert_eq!(
113            from_specs, from_enum,
114            "RuleId and the specs disagree; update the enum and the specs together"
115        );
116    }
117
118    #[test]
119    fn every_profile_projection_resolves_to_an_embedded_asset() {
120        for id in [ProfileId::Codebase, ProfileId::KnowledgeBase] {
121            let profile = id.profile();
122            for entry in profile.managed.iter().chain(profile.adopted) {
123                assert!(
124                    asset(entry.source).is_some(),
125                    "{id}: {} is not embedded",
126                    entry.source
127                );
128            }
129        }
130    }
131
132    #[test]
133    fn the_method_and_licenses_are_carried() {
134        assert!(METHOD.get_file("glossary.md").is_some());
135        assert!(METHOD.files().count() >= 15);
136        assert!(LICENSE.contains("LICENSE-MIT") && LICENSE.contains("LICENSE-CC-BY-4.0"));
137    }
138
139    #[test]
140    fn rule_id_parser_matches_heading_shape_only() {
141        let text = "### `a-b:c-d` — Title\n### `Bad:Id`\nplain ### `x:y`\n### `no-colon`\n";
142        let ids: Vec<String> = rule_ids_in(text).collect();
143        assert_eq!(ids, vec!["a-b:c-d".to_string()]);
144    }
145}