spec_driven_docs/
embedded.rs1use std::collections::BTreeSet;
10
11use include_dir::{Dir, include_dir};
12
13pub static SPECS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/_docs/specs");
15pub static TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates");
17pub static MARKDOWNLINT: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/.markdownlint");
19pub static SNIPPETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/instance/snippets");
21pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
23pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
25
26pub static LICENSE: &str = include_str!("../LICENSE");
28pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
30pub 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#[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#[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#[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#[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
83pub 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}