1use std::sync::OnceLock;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct SkillFrontmatter {
12 pub name: &'static str,
13 pub short: &'static str,
14 pub description: &'static str,
15 pub when_to_use: Option<&'static str>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct EmbeddedSkill {
21 pub name: &'static str,
22 pub frontmatter: SkillFrontmatter,
23 pub body: &'static str,
24 pub source: &'static str,
29}
30
31const SOURCES: &[&str] = &[
32 include_str!("corpus/harn-agent/SKILL.md"),
33 include_str!("corpus/harn-diagnostics/SKILL.md"),
34 include_str!("corpus/harn-language/SKILL.md"),
35 include_str!("corpus/harn-orchestration/SKILL.md"),
36 include_str!("corpus/harn-providers/SKILL.md"),
37 include_str!("corpus/harn-testing/SKILL.md"),
38 include_str!("corpus/harn-tracing/SKILL.md"),
39];
40
41static EMBEDDED_SKILLS: OnceLock<Box<[EmbeddedSkill]>> = OnceLock::new();
42
43pub fn list_embedded_skills() -> &'static [EmbeddedSkill] {
45 EMBEDDED_SKILLS
46 .get_or_init(|| SOURCES.iter().map(|source| parse_skill(source)).collect())
47 .as_ref()
48}
49
50pub fn get_embedded_skill(name: &str) -> Option<&'static EmbeddedSkill> {
52 list_embedded_skills()
53 .iter()
54 .find(|skill| skill.name == name)
55}
56
57fn parse_skill(source: &'static str) -> EmbeddedSkill {
58 let (frontmatter, body) = split_frontmatter(source);
59 let frontmatter = parse_frontmatter(frontmatter);
60 EmbeddedSkill {
61 name: frontmatter.name,
62 frontmatter,
63 body,
64 source,
65 }
66}
67
68fn split_frontmatter(source: &'static str) -> (&'static str, &'static str) {
69 let Some(after_open) = source.strip_prefix("---\n") else {
70 panic!("embedded skill source is missing opening frontmatter delimiter");
71 };
72 let Some(close_offset) = after_open.find("\n---\n") else {
73 panic!("embedded skill source is missing closing frontmatter delimiter");
74 };
75 (
76 &after_open[..close_offset],
77 &after_open[close_offset + "\n---\n".len()..],
78 )
79}
80
81fn parse_frontmatter(frontmatter: &'static str) -> SkillFrontmatter {
82 let mut name = None;
83 let mut short = None;
84 let mut description = None;
85 let mut when_to_use = None;
86
87 for line in frontmatter.lines() {
88 let Some((key, value)) = line.split_once(':') else {
89 continue;
90 };
91 let value = value.trim();
92 match key {
93 "name" => name = Some(value),
94 "short" => short = Some(value),
95 "description" => description = Some(value),
96 "when_to_use" => when_to_use = Some(value),
97 _ => {}
98 }
99 }
100
101 SkillFrontmatter {
102 name: name.expect("embedded skill frontmatter is missing `name`"),
103 short: short.expect("embedded skill frontmatter is missing `short`"),
104 description: description.expect("embedded skill frontmatter is missing `description`"),
105 when_to_use,
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use std::collections::BTreeSet;
113
114 #[test]
115 fn lists_expected_initial_corpus() {
116 let skills = list_embedded_skills();
117 assert!(skills.len() >= 7);
118 assert_eq!(skills.len(), SOURCES.len());
119 }
120
121 #[test]
122 fn can_fetch_harn_language_skill() {
123 let skill = get_embedded_skill("harn-language").expect("harn-language skill is embedded");
124 assert_eq!(skill.frontmatter.name, "harn-language");
125 assert!(skill.body.contains("Harn language"));
126 }
127
128 #[test]
129 fn skills_have_unique_names_and_body_only_content() {
130 let mut names = BTreeSet::new();
131 for skill in list_embedded_skills() {
132 assert_eq!(skill.name, skill.frontmatter.name);
133 assert!(names.insert(skill.name), "duplicate skill {}", skill.name);
134 assert!(
135 !skill.body.trim().is_empty(),
136 "{} body is empty",
137 skill.name
138 );
139 assert!(
140 !skill.body.trim_start().starts_with("---"),
141 "{} body includes frontmatter",
142 skill.name
143 );
144 }
145 }
146
147 #[test]
148 fn skills_are_sorted_by_name() {
149 let names: Vec<&str> = list_embedded_skills()
150 .iter()
151 .map(|skill| skill.name)
152 .collect();
153 let mut sorted = names.clone();
154 sorted.sort_unstable();
155 assert_eq!(names, sorted);
156 }
157
158 #[test]
159 fn source_round_trips_to_frontmatter_and_body() {
160 for skill in list_embedded_skills() {
161 assert!(
162 skill.source.starts_with("---\n"),
163 "{} source missing opening fence",
164 skill.name
165 );
166 assert!(
167 skill.source.ends_with(skill.body),
168 "{} source must end with the body so dump output is byte-stable",
169 skill.name
170 );
171 assert!(
172 skill.source.contains(&format!("name: {}\n", skill.name)),
173 "{} source missing canonical name field",
174 skill.name
175 );
176 }
177 }
178
179 #[test]
180 fn embedded_corpus_stays_within_binary_budget() {
181 let bytes: usize = SOURCES.iter().map(|source| source.len()).sum();
182 assert!(
183 bytes <= 200 * 1024,
184 "embedded corpus is {bytes} bytes, expected <= 200 KiB"
185 );
186 }
187}