spec_driven_docs/
embedded.rs1use std::collections::BTreeSet;
10
11use include_dir::{Dir, include_dir};
12
13pub use crate::payload_roots::PAYLOAD_ROOTS;
14
15pub static SPECS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/_docs/specs");
17pub static TEMPLATES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/templates");
19pub static MARKDOWNLINT: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/.markdownlint");
21pub static SNIPPETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/instance/snippets");
23pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
25pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
27pub static SKILL_SHARED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skill-shared");
29pub static SIMPLE_ENGLISH: Dir<'static> =
31 include_dir!("$CARGO_MANIFEST_DIR/third-party/simpleenglish");
32
33pub static LICENSE: &str = include_str!("../LICENSE");
35pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
37pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
39pub static THIRD_PARTY_NOTICES: &str = include_str!("../THIRD_PARTY_NOTICES.md");
41
42const EMBEDDED_ROOTS: &[(&str, &Dir<'static>)] = &[
47 ("_docs/specs", &SPECS),
48 ("templates", &TEMPLATES),
49 (".markdownlint", &MARKDOWNLINT),
50 ("instance/snippets", &SNIPPETS),
51 ("method", &METHOD),
52 ("skills", &SKILLS),
53 ("skill-shared", &SKILL_SHARED),
54 ("third-party/simpleenglish", &SIMPLE_ENGLISH),
55];
56
57#[must_use]
59pub fn skill_names() -> Vec<&'static str> {
60 let mut names: Vec<&'static str> = SKILLS
61 .dirs()
62 .filter_map(|dir| dir.path().as_os_str().to_str())
63 .collect();
64 names.sort_unstable();
65 names
66}
67
68#[must_use]
70pub fn skill(name: &str) -> Option<&'static str> {
71 SKILLS
72 .get_file(format!("{name}/SKILL.md"))
73 .and_then(include_dir::File::contents_utf8)
74}
75
76#[must_use]
83pub fn shared_artifacts() -> Vec<(String, &'static [u8])> {
84 fn walk(dir: &Dir<'static>, out: &mut Vec<(String, &'static [u8])>) {
85 for file in dir.files() {
86 if let Some(path) = file.path().to_str() {
87 out.push((path.to_string(), file.contents()));
88 }
89 }
90 for sub in dir.dirs() {
91 walk(sub, out);
92 }
93 }
94 let mut out = Vec::new();
95 walk(&SKILL_SHARED, &mut out);
96 out.sort_by(|a, b| a.0.cmp(&b.0));
97 out
98}
99
100#[must_use]
103pub fn asset(source: &str) -> Option<&'static [u8]> {
104 EMBEDDED_ROOTS.iter().find_map(|(root, dir)| {
105 let rest = source.strip_prefix(root)?.strip_prefix('/')?;
106 dir.get_file(rest).map(include_dir::File::contents)
107 })
108}
109
110#[must_use]
112pub fn spec_rule_ids() -> BTreeSet<String> {
113 let mut ids = BTreeSet::new();
114 for file in SPECS.files() {
115 let Some(text) = file.contents_utf8() else {
116 continue;
117 };
118 ids.extend(rule_ids_in(text));
119 }
120 ids
121}
122
123pub fn rule_ids_in(text: &str) -> impl Iterator<Item = String> + '_ {
125 text.lines().filter_map(|line| {
126 let candidate = line.strip_prefix("### `")?;
127 let (id, _) = candidate.split_once('`')?;
128 let (domain, rule) = id.split_once(':')?;
129 let is_slug = |part: &str| {
130 !part.is_empty()
131 && part
132 .bytes()
133 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
134 };
135 (is_slug(domain) && is_slug(rule)).then(|| id.to_string())
136 })
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::domain::profile::ProfileId;
143 use crate::domain::rule_id::RuleId;
144
145 #[test]
146 fn rule_id_enum_matches_the_embedded_specs() {
147 let from_specs = spec_rule_ids();
148 let from_enum: BTreeSet<String> = RuleId::ALL
149 .iter()
150 .map(|rule| rule.as_str().to_string())
151 .collect();
152 assert_eq!(
153 from_specs, from_enum,
154 "RuleId and the specs disagree; update the enum and the specs together"
155 );
156 }
157
158 #[test]
159 fn the_embedded_roots_are_the_declared_payload_roots() {
160 let embedded: Vec<&str> = EMBEDDED_ROOTS.iter().map(|(root, _)| *root).collect();
161 assert_eq!(
162 embedded,
163 PAYLOAD_ROOTS.to_vec(),
164 "payload_roots.rs and the embedded statics disagree; a root missing from the declaration ships unscanned"
165 );
166 }
167
168 #[test]
169 fn every_profile_projection_resolves_to_an_embedded_asset() {
170 for id in [ProfileId::Codebase, ProfileId::KnowledgeBase] {
171 let profile = id.profile();
172 for entry in profile.managed.iter().chain(profile.adopted) {
173 assert!(
174 asset(entry.source).is_some(),
175 "{id}: {} is not embedded",
176 entry.source
177 );
178 }
179 }
180 }
181
182 #[test]
183 fn the_method_and_licenses_are_carried() {
184 assert!(METHOD.get_file("glossary.md").is_some());
185 assert!(METHOD.files().count() >= 15);
186 assert!(LICENSE.contains("LICENSE-MIT") && LICENSE.contains("LICENSE-CC-BY-4.0"));
187 }
188
189 #[test]
190 fn rule_id_parser_matches_heading_shape_only() {
191 let text = "### `a-b:c-d` — Title\n### `Bad:Id`\nplain ### `x:y`\n### `no-colon`\n";
192 let ids: Vec<String> = rule_ids_in(text).collect();
193 assert_eq!(ids, vec!["a-b:c-d".to_string()]);
194 }
195}