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 INSTANCE: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/instance");
28pub static GUIDANCE: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/guidance");
30pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
32pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
34pub static SKILL_SHARED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skill-shared");
36
37pub static LICENSE: &str = include_str!("../LICENSE");
39pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
41pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
43pub static THIRD_PARTY_NOTICES: &str = include_str!("../THIRD_PARTY_NOTICES.md");
45
46const EMBEDDED_ROOTS: &[(&str, &Dir<'static>)] = &[
51 ("_docs/specs", &SPECS),
52 ("templates", &TEMPLATES),
53 (".markdownlint", &MARKDOWNLINT),
54 ("instance", &INSTANCE),
55 ("guidance", &GUIDANCE),
56 ("method", &METHOD),
57 ("skills", &SKILLS),
58 ("skill-shared", &SKILL_SHARED),
59];
60
61#[must_use]
66pub const fn roots() -> &'static [(&'static str, &'static Dir<'static>)] {
67 EMBEDDED_ROOTS
68}
69
70#[must_use]
72pub fn skill_names() -> Vec<&'static str> {
73 let mut names: Vec<&'static str> = SKILLS
74 .dirs()
75 .filter_map(|dir| dir.path().as_os_str().to_str())
76 .collect();
77 names.sort_unstable();
78 names
79}
80
81#[must_use]
83pub fn skill(name: &str) -> Option<&'static str> {
84 SKILLS
85 .get_file(format!("{name}/SKILL.md"))
86 .and_then(include_dir::File::contents_utf8)
87}
88
89#[must_use]
98pub fn skill_package(name: &str) -> Option<Vec<(String, &'static [u8])>> {
99 use crate::domain::paths::{SKILL_FILE, SKILL_REFERENCES_DIR};
100
101 let manual = SKILLS.get_file(format!("{name}/{SKILL_FILE}"))?;
102 let mut files = vec![(SKILL_FILE.to_string(), manual.contents())];
103 for (path, bytes) in shared_artifacts() {
104 files.push((format!("{SKILL_REFERENCES_DIR}/{path}"), bytes));
105 }
106 files.sort_by(|left, right| left.0.cmp(&right.0));
107 Some(files)
108}
109
110#[must_use]
117pub fn shared_artifacts() -> Vec<(String, &'static [u8])> {
118 fn walk(dir: &Dir<'static>, out: &mut Vec<(String, &'static [u8])>) {
119 for file in dir.files() {
120 if let Some(path) = file.path().to_str() {
121 out.push((path.to_string(), file.contents()));
122 }
123 }
124 for sub in dir.dirs() {
125 walk(sub, out);
126 }
127 }
128 let mut out = Vec::new();
129 walk(&SKILL_SHARED, &mut out);
130 out.sort_by(|a, b| a.0.cmp(&b.0));
131 out
132}
133
134#[must_use]
137pub fn asset(source: &str) -> Option<&'static [u8]> {
138 EMBEDDED_ROOTS.iter().find_map(|(root, dir)| {
139 let rest = source.strip_prefix(root)?.strip_prefix('/')?;
140 dir.get_file(rest).map(include_dir::File::contents)
141 })
142}
143
144#[must_use]
146pub fn spec_rule_ids() -> BTreeSet<String> {
147 let mut ids = BTreeSet::new();
148 for file in SPECS.files() {
149 let Some(text) = file.contents_utf8() else {
150 continue;
151 };
152 ids.extend(rule_ids_in(text));
153 }
154 ids
155}
156
157pub fn rule_ids_in(text: &str) -> impl Iterator<Item = String> + '_ {
159 text.lines().filter_map(|line| {
160 let candidate = line.strip_prefix("### `")?;
161 let (id, _) = candidate.split_once('`')?;
162 let (domain, rule) = id.split_once(':')?;
163 let is_slug = |part: &str| {
164 !part.is_empty()
165 && part
166 .bytes()
167 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
168 };
169 (is_slug(domain) && is_slug(rule)).then(|| id.to_string())
170 })
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::domain::profile::ProfileId;
177 use crate::domain::rule_id::RuleId;
178
179 #[test]
180 fn rule_id_enum_matches_the_embedded_specs() {
181 let from_specs = spec_rule_ids();
182 let from_enum: BTreeSet<String> = RuleId::ALL
183 .iter()
184 .map(|rule| rule.as_str().to_string())
185 .collect();
186 assert_eq!(
187 from_specs, from_enum,
188 "RuleId and the specs disagree; update the enum and the specs together"
189 );
190 }
191
192 #[test]
193 fn the_embedded_roots_are_the_declared_payload_roots() {
194 let embedded: Vec<&str> = EMBEDDED_ROOTS.iter().map(|(root, _)| *root).collect();
195 assert_eq!(
196 embedded,
197 PAYLOAD_ROOTS.to_vec(),
198 "payload_roots.rs and the embedded statics disagree; a root missing from the declaration ships unscanned"
199 );
200 }
201
202 #[test]
203 fn every_profile_projection_resolves_to_an_embedded_asset() {
204 for id in [ProfileId::Codebase, ProfileId::KnowledgeBase] {
205 let profile = id.profile();
206 for entry in profile.managed.iter().chain(profile.adopted) {
207 assert!(
208 asset(&entry.source).is_some(),
209 "{id}: {} is not embedded",
210 entry.source
211 );
212 }
213 }
214 }
215
216 #[test]
217 fn the_method_and_licenses_are_carried() {
218 assert!(METHOD.get_file("glossary.md").is_some());
219 assert!(METHOD.files().count() >= 15);
220 assert!(LICENSE.contains("LICENSE-MIT") && LICENSE.contains("LICENSE-CC-BY-4.0"));
221 }
222
223 #[test]
224 fn rule_id_parser_matches_heading_shape_only() {
225 let text = "### `a-b:c-d` — Title\n### `Bad:Id`\nplain ### `x:y`\n### `no-colon`\n";
226 let ids: Vec<String> = rule_ids_in(text).collect();
227 assert_eq!(ids, vec!["a-b:c-d".to_string()]);
228 }
229}