Skip to main content

harn_skills/
lib.rs

1//! Embedded Harn skill corpus.
2//!
3//! This crate exposes the bundled corpus as metadata plus `SKILL.md`
4//! bodies. CLI commands that enumerate, dump, or install these skills
5//! are layered above this foundation.
6
7use std::sync::OnceLock;
8
9/// Frontmatter fields embedded with each bundled skill.
10#[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/// A single skill embedded into the Harn build.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct EmbeddedSkill {
21    pub name: &'static str,
22    pub frontmatter: SkillFrontmatter,
23    pub body: &'static str,
24    /// The full original SKILL.md source — frontmatter delimiter,
25    /// frontmatter block, blank line, and body — exactly as embedded.
26    /// Use this when round-tripping a skill back to disk so the dumped
27    /// copy is byte-identical to the binary's canonical record.
28    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
43/// Return every skill bundled into this build.
44pub 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
50/// Return one bundled skill by canonical skill name.
51pub 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}