Skip to main content

oxicode/discovery/builtin_rules/
mod.rs

1//! Compile-time embedded TTSR rules for Rust projects.
2//!
3//! Each `.md` file in this directory is a TTSR rule with YAML
4//! frontmatter and markdown body. They are embedded at compile time
5//! via `include_str!()`.
6
7use oxicode_agent::agent_loop::ttsr::{InterruptMode, Rule, RuleSource, ScopeToken};
8use regex::Regex;
9use serde::Deserialize;
10
11/// Raw content of bundled rule files: (filename stem, content).
12fn raw_bundled_rules() -> Vec<(&'static str, &'static str)> {
13    vec![
14        ("rs-future-prelude", include_str!("rs-future-prelude.md")),
15        ("rs-box-leak", include_str!("rs-box-leak.md")),
16        (
17            "rs-match-ergonomics",
18            include_str!("rs-match-ergonomics.md"),
19        ),
20        ("rs-parking-lot", include_str!("rs-parking-lot.md")),
21        ("rs-result-type", include_str!("rs-result-type.md")),
22        ("rs-lazylock", include_str!("rs-lazylock.md")),
23        ("rs-tokio-mutex", include_str!("rs-tokio-mutex.md")),
24    ]
25}
26
27/// YAML frontmatter parsed from a `.mdc` rule file.
28#[derive(Debug, Deserialize)]
29struct RuleFrontmatter {
30    description: Option<String>,
31    condition: String,
32    scope: Option<String>,
33    #[serde(rename = "interruptMode")]
34    interrupt_mode: Option<String>,
35    globs: Option<Vec<String>>,
36    #[serde(rename = "alwaysApply")]
37    always_apply: Option<bool>,
38}
39
40/// Map a frontmatter scope string to [`ScopeToken`] values.
41fn parse_scope(s: &str) -> Vec<ScopeToken> {
42    match s.trim().to_lowercase().as_str() {
43        "text" => vec![ScopeToken::Text],
44        "thinking" => vec![ScopeToken::Thinking],
45        "tool" => vec![ScopeToken::Tool {
46            name: String::new(),
47            globs: vec![],
48        }],
49        _ => vec![ScopeToken::Text],
50    }
51}
52
53/// Map a frontmatter interrupt-mode string to [`InterruptMode`].
54fn parse_interrupt_mode(s: &str) -> InterruptMode {
55    match s.trim().to_lowercase().as_str() {
56        "never" => InterruptMode::Never,
57        "prose-only" | "proseonly" => InterruptMode::ProseOnly,
58        "tool-only" | "toolonly" => InterruptMode::ToolOnly,
59        "always" => InterruptMode::Always,
60        _ => InterruptMode::ProseOnly,
61    }
62}
63
64/// Parse a single rule file from its raw content.
65///
66/// Returns `None` if the frontmatter is missing, malformed, or the
67/// regex condition fails to compile.
68pub(crate) fn parse_rule_file(content: &str, name: &str, source: RuleSource) -> Option<Rule> {
69    // Split on the first two `---` separators (frontmatter delimiters).
70    let parts: Vec<&str> = content.splitn(3, "---").collect();
71    if parts.len() < 3 {
72        return None;
73    }
74
75    let yaml_str = parts[1].trim();
76    let body = parts[2].trim();
77
78    let fm: RuleFrontmatter = serde_yaml::from_str(yaml_str).ok()?;
79    let condition = Regex::new(&fm.condition).ok()?;
80
81    let scope = parse_scope(fm.scope.as_deref().unwrap_or("text"));
82    let interrupt_mode = parse_interrupt_mode(fm.interrupt_mode.as_deref().unwrap_or("prose-only"));
83
84    Some(Rule {
85        name: name.to_string(),
86        content: body.to_string(),
87        description: fm.description,
88        condition: vec![condition],
89        scope,
90        interrupt_mode,
91        globs: fm.globs.unwrap_or_default(),
92        always_apply: fm.always_apply.unwrap_or(false),
93        source,
94        ast_condition: None,
95    })
96}
97
98/// Load all bundled builtin rules.
99///
100/// These are the Rust-specific rules shipped with oxicode-cli. They are
101/// always available regardless of project configuration.
102pub fn load_all() -> Vec<Rule> {
103    raw_bundled_rules()
104        .into_iter()
105        .filter_map(|(name, content)| parse_rule_file(content, name, RuleSource::BuiltinDefaults))
106        .collect()
107}