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 METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
30pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
32pub static SKILL_SHARED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skill-shared");
34pub static COMPARISON_DOCS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/comparison-docs");
36pub static PRIOR_ART: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/reference/prior-art");
38pub static TRACKER_MARKUP: Dir<'static> =
40 include_dir!("$CARGO_MANIFEST_DIR/reference/tracker-markup");
41
42pub static LICENSE: &str = include_str!("../LICENSE");
44pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
46pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
48pub static THIRD_PARTY_NOTICES: &str = include_str!("../THIRD_PARTY_NOTICES.md");
50pub static CHANGELOG: &str = include_str!("../CHANGELOG.md");
52
53const EMBEDDED_ROOTS: &[(&str, &Dir<'static>)] = &[
58 ("_docs/specs", &SPECS),
59 ("templates", &TEMPLATES),
60 (".markdownlint", &MARKDOWNLINT),
61 ("instance", &INSTANCE),
62 ("method", &METHOD),
63 ("skills", &SKILLS),
64 ("skill-shared", &SKILL_SHARED),
65 ("comparison-docs", &COMPARISON_DOCS),
66 ("reference/prior-art", &PRIOR_ART),
67 ("reference/tracker-markup", &TRACKER_MARKUP),
68];
69
70#[must_use]
75pub const fn roots() -> &'static [(&'static str, &'static Dir<'static>)] {
76 EMBEDDED_ROOTS
77}
78
79#[must_use]
81pub fn skill_names() -> Vec<&'static str> {
82 let mut names: Vec<&'static str> = SKILLS
83 .dirs()
84 .filter_map(|dir| dir.path().as_os_str().to_str())
85 .collect();
86 names.sort_unstable();
87 names
88}
89
90#[must_use]
92pub fn skill(name: &str) -> Option<&'static str> {
93 SKILLS
94 .get_file(format!("{name}/SKILL.md"))
95 .and_then(include_dir::File::contents_utf8)
96}
97
98#[must_use]
107pub fn skill_package(name: &str) -> Option<Vec<(String, &'static [u8])>> {
108 use crate::domain::paths::{SKILL_FILE, SKILL_REFERENCES_DIR};
109
110 let manual = SKILLS.get_file(format!("{name}/{SKILL_FILE}"))?;
111 let mut files = vec![(SKILL_FILE.to_string(), manual.contents())];
112 for (path, bytes) in shared_artifacts() {
113 files.push((format!("{SKILL_REFERENCES_DIR}/{path}"), bytes));
114 }
115 files.sort_by(|left, right| left.0.cmp(&right.0));
116 Some(files)
117}
118
119#[must_use]
126pub fn shared_artifacts() -> Vec<(String, &'static [u8])> {
127 fn walk(dir: &Dir<'static>, out: &mut Vec<(String, &'static [u8])>) {
128 for file in dir.files() {
129 if let Some(path) = file.path().to_str() {
130 out.push((path.to_string(), file.contents()));
131 }
132 }
133 for sub in dir.dirs() {
134 walk(sub, out);
135 }
136 }
137 let mut out = Vec::new();
138 walk(&SKILL_SHARED, &mut out);
139 out.sort_by(|a, b| a.0.cmp(&b.0));
140 out
141}
142
143#[must_use]
146pub fn asset(source: &str) -> Option<&'static [u8]> {
147 EMBEDDED_ROOTS.iter().find_map(|(root, dir)| {
148 let rest = source.strip_prefix(root)?.strip_prefix('/')?;
149 dir.get_file(rest).map(include_dir::File::contents)
150 })
151}
152
153#[must_use]
158pub fn assets_under(root: &str) -> Vec<(String, &'static [u8])> {
159 let Some((name, dir)) = EMBEDDED_ROOTS.iter().find(|(name, _)| *name == root) else {
160 return Vec::new();
161 };
162 let mut out = Vec::new();
163 collect_under(name, dir, &mut out);
164 out.sort_by(|a, b| a.0.cmp(&b.0));
165 out
166}
167
168fn collect_under(root: &str, dir: &'static Dir<'static>, out: &mut Vec<(String, &'static [u8])>) {
169 for file in dir.files() {
170 if let Some(rest) = file.path().to_str() {
171 out.push((format!("{root}/{rest}"), file.contents()));
172 }
173 }
174 for sub in dir.dirs() {
175 collect_under(root, sub, out);
176 }
177}
178
179#[must_use]
181pub fn spec_rule_ids() -> BTreeSet<String> {
182 let mut ids = BTreeSet::new();
183 for file in SPECS.files() {
184 let Some(text) = file.contents_utf8() else {
185 continue;
186 };
187 ids.extend(rule_ids_in(text));
188 }
189 ids
190}
191
192pub fn rule_ids_in(text: &str) -> impl Iterator<Item = String> + '_ {
194 text.lines().filter_map(|line| {
195 let candidate = line.strip_prefix("### `")?;
196 let (id, _) = candidate.split_once('`')?;
197 let (domain, rule) = id.split_once(':')?;
198 let is_slug = |part: &str| {
199 !part.is_empty()
200 && part
201 .bytes()
202 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
203 };
204 (is_slug(domain) && is_slug(rule)).then(|| id.to_string())
205 })
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::domain::profile::ProfileId;
212 use crate::domain::rule_id::RuleId;
213
214 #[test]
215 fn rule_id_enum_matches_the_embedded_specs() {
216 let from_specs = spec_rule_ids();
217 let from_enum: BTreeSet<String> = RuleId::ALL
218 .iter()
219 .map(|rule| rule.as_str().to_string())
220 .collect();
221 assert_eq!(
222 from_specs, from_enum,
223 "RuleId and the specs disagree; update the enum and the specs together"
224 );
225 }
226
227 #[test]
228 fn the_embedded_roots_are_the_declared_payload_roots() {
229 let embedded: Vec<&str> = EMBEDDED_ROOTS.iter().map(|(root, _)| *root).collect();
230 assert_eq!(
231 embedded,
232 PAYLOAD_ROOTS.to_vec(),
233 "payload_roots.rs and the embedded statics disagree; a root missing from the declaration ships unscanned"
234 );
235 }
236
237 #[test]
238 fn every_profile_projection_resolves_to_an_embedded_asset() {
239 for id in [ProfileId::Codebase, ProfileId::KnowledgeBase] {
240 let profile = id.profile();
241 for entry in profile.managed.iter().chain(profile.adopted) {
242 assert!(
243 asset(&entry.source).is_some(),
244 "{id}: {} is not embedded",
245 entry.source
246 );
247 }
248 }
249 }
250
251 #[test]
252 fn the_method_and_licenses_are_carried() {
253 assert!(METHOD.get_file("glossary.md").is_some());
254 assert!(METHOD.files().count() >= 15);
255 assert!(LICENSE.contains("LICENSE-MIT") && LICENSE.contains("LICENSE-CC-BY-4.0"));
256 }
257
258 #[test]
259 fn rule_id_parser_matches_heading_shape_only() {
260 let text = "### `a-b:c-d` — Title\n### `Bad:Id`\nplain ### `x:y`\n### `no-colon`\n";
261 let ids: Vec<String> = rule_ids_in(text).collect();
262 assert_eq!(ids, vec!["a-b:c-d".to_string()]);
263 }
264}