Skip to main content

mur_common/skill/
types.rs

1//! Skill type enums.
2
3use serde::{Deserialize, Serialize};
4
5/// Which host(s) may load a skill. See spec §2.3.
6#[derive(
7    Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default, schemars::JsonSchema,
8)]
9#[serde(rename_all = "kebab-case")]
10pub enum HostId {
11    MurAgent,
12    MurCommander,
13    /// Default when `hosts:` is omitted — backward compatible.
14    #[default]
15    All,
16    #[serde(untagged)]
17    Custom(String),
18}
19
20/// Three-tier skill trust model. Mirrors mur-commander `trust/level.rs`.
21#[derive(
22    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, Default,
23)]
24#[serde(rename_all = "kebab-case")]
25pub enum TrustLevel {
26    /// Peer transfer, agent-generated, untrusted registry.
27    #[default]
28    Sandboxed,
29    /// Registry-verified checksum match, community-reviewed.
30    Verified,
31    /// Built-in, user-promoted, or trusted-publisher-signed.
32    Trusted,
33}
34
35/// Top-level skill category.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
37#[serde(rename_all = "lowercase")]
38pub enum Category {
39    Context,
40    Workflow,
41    Command,
42    Meta,
43    Note,
44    /// Media skills (video-analyze, scene-explain, vlc-control, watch-together):
45    /// they drive the runtime's media tools rather than the four-stage pipeline.
46    Media,
47}
48
49/// Publishers whose on-disk copies MUR owns and may replace: the shipped
50/// builtins and the official catalog. Everything else — `human:local` from
51/// `mur notes create`, `agent:<name>` from the `remember` tool, a `human:<you>`
52/// skill you authored — is yours, and MUR cannot get it back if it removes it.
53///
54/// The list already existed in `sync_cmd` for "may I overwrite this?"; the
55/// lifecycle sweep needs the same question for "may I delete this?", and two
56/// copies of one list is how the answers start disagreeing.
57pub const MUR_OWNED_PUBLISHERS: &[&str] = &["human:mur-official", "human:mur"];
58
59/// Whether MUR published this and can therefore reinstall it.
60pub fn is_mur_owned_publisher(publisher: &str) -> bool {
61    MUR_OWNED_PUBLISHERS.contains(&publisher)
62}
63
64/// Where a skill came from. Drives the curation gate: `Llm`-authored skills
65/// cannot auto-promote past `Emerging` until a human curates them
66/// (amendment A1, `2026-05-28-mur-workflow-engine-design-v2.md`).
67#[derive(
68    Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, schemars::JsonSchema,
69)]
70#[serde(rename_all = "lowercase")]
71pub enum Provenance {
72    /// Hand-authored by a person. Default — no gate.
73    #[default]
74    Human,
75    /// Produced by the LLM extraction judge. Gated until curated.
76    Llm,
77    /// LLM-extracted, then human-reviewed/edited. No gate.
78    Hybrid,
79}
80
81/// Exactly one content mode is populated; see spec §3.2.3.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "lowercase")]
84pub enum ContentMode {
85    Context,
86    Workflow,
87    Command,
88    Note,
89}
90
91#[derive(
92    Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, schemars::JsonSchema,
93)]
94#[serde(rename_all = "lowercase")]
95pub enum Priority {
96    Low,
97    #[default]
98    Normal,
99    High,
100    Critical,
101}
102
103#[derive(
104    Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd, schemars::JsonSchema,
105)]
106#[serde(rename_all = "snake_case")]
107pub enum TriggerKind {
108    Command,
109    Keyword,
110    SessionStart,
111    Manual,
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn host_id_serialises_kebab_case() {
120        let yaml = serde_yaml_ng::to_string(&HostId::MurAgent).unwrap();
121        assert_eq!(yaml.trim(), "mur-agent");
122    }
123
124    #[test]
125    fn trust_level_ordering_matches_spec() {
126        assert!(TrustLevel::Sandboxed < TrustLevel::Verified);
127        assert!(TrustLevel::Verified < TrustLevel::Trusted);
128    }
129
130    #[test]
131    fn host_id_default_is_all() {
132        assert_eq!(HostId::default(), HostId::All);
133    }
134
135    #[test]
136    fn note_category_serialises_lowercase_and_roundtrips() {
137        let yaml = serde_yaml_ng::to_string(&Category::Note).unwrap();
138        assert_eq!(yaml.trim(), "note");
139        let parsed: Category = serde_yaml_ng::from_str("note").unwrap();
140        assert_eq!(parsed, Category::Note);
141    }
142
143    #[test]
144    fn media_category_serialises_lowercase_and_roundtrips() {
145        let yaml = serde_yaml_ng::to_string(&Category::Media).unwrap();
146        assert_eq!(yaml.trim(), "media");
147        let parsed: Category = serde_yaml_ng::from_str("media").unwrap();
148        assert_eq!(parsed, Category::Media);
149    }
150
151    #[test]
152    fn note_content_mode_serialises_lowercase_and_roundtrips() {
153        let yaml = serde_yaml_ng::to_string(&ContentMode::Note).unwrap();
154        assert_eq!(yaml.trim(), "note");
155        let parsed: ContentMode = serde_yaml_ng::from_str("note").unwrap();
156        assert_eq!(parsed, ContentMode::Note);
157    }
158
159    #[test]
160    fn provenance_defaults_to_human_and_roundtrips() {
161        // Default is Human (a skill is human-authored unless stated otherwise).
162        assert_eq!(Provenance::default(), Provenance::Human);
163        // Serializes lowercase, like Category.
164        let yaml = serde_yaml_ng::to_string(&Provenance::Llm).unwrap();
165        assert_eq!(yaml.trim(), "llm");
166        let parsed: Provenance = serde_yaml_ng::from_str("hybrid").unwrap();
167        assert_eq!(parsed, Provenance::Hybrid);
168    }
169}