use oxi_agent::agent_loop::ttsr::{InterruptMode, Rule, RuleSource, ScopeToken};
use regex::Regex;
use serde::Deserialize;
fn raw_bundled_rules() -> Vec<(&'static str, &'static str)> {
vec![
("rs-future-prelude", include_str!("rs-future-prelude.md")),
("rs-box-leak", include_str!("rs-box-leak.md")),
(
"rs-match-ergonomics",
include_str!("rs-match-ergonomics.md"),
),
("rs-parking-lot", include_str!("rs-parking-lot.md")),
("rs-result-type", include_str!("rs-result-type.md")),
("rs-lazylock", include_str!("rs-lazylock.md")),
("rs-tokio-mutex", include_str!("rs-tokio-mutex.md")),
]
}
#[derive(Debug, Deserialize)]
struct RuleFrontmatter {
description: Option<String>,
condition: String,
scope: Option<String>,
#[serde(rename = "interruptMode")]
interrupt_mode: Option<String>,
globs: Option<Vec<String>>,
#[serde(rename = "alwaysApply")]
always_apply: Option<bool>,
}
fn parse_scope(s: &str) -> Vec<ScopeToken> {
match s.trim().to_lowercase().as_str() {
"text" => vec![ScopeToken::Text],
"thinking" => vec![ScopeToken::Thinking],
"tool" => vec![ScopeToken::Tool {
name: String::new(),
globs: vec![],
}],
_ => vec![ScopeToken::Text],
}
}
fn parse_interrupt_mode(s: &str) -> InterruptMode {
match s.trim().to_lowercase().as_str() {
"never" => InterruptMode::Never,
"prose-only" | "proseonly" => InterruptMode::ProseOnly,
"tool-only" | "toolonly" => InterruptMode::ToolOnly,
"always" => InterruptMode::Always,
_ => InterruptMode::ProseOnly,
}
}
pub(crate) fn parse_rule_file(content: &str, name: &str, source: RuleSource) -> Option<Rule> {
let parts: Vec<&str> = content.splitn(3, "---").collect();
if parts.len() < 3 {
return None;
}
let yaml_str = parts[1].trim();
let body = parts[2].trim();
let fm: RuleFrontmatter = serde_yaml::from_str(yaml_str).ok()?;
let condition = Regex::new(&fm.condition).ok()?;
let scope = parse_scope(fm.scope.as_deref().unwrap_or("text"));
let interrupt_mode = parse_interrupt_mode(fm.interrupt_mode.as_deref().unwrap_or("prose-only"));
Some(Rule {
name: name.to_string(),
content: body.to_string(),
description: fm.description,
condition: vec![condition],
scope,
interrupt_mode,
globs: fm.globs.unwrap_or_default(),
always_apply: fm.always_apply.unwrap_or(false),
source,
})
}
pub fn load_all() -> Vec<Rule> {
raw_bundled_rules()
.into_iter()
.filter_map(|(name, content)| parse_rule_file(content, name, RuleSource::BuiltinDefaults))
.collect()
}