1use super::builtin_rules;
25use oxi_agent::agent_loop::ttsr::Rule;
26use oxi_agent::agent_loop::ttsr::RuleSource;
27use std::path::Path;
28
29pub fn discover_rules(project_dir: &Path) -> Vec<Rule> {
36 let mut rules = Vec::new();
37
38 rules.extend(builtin_rules::load_all());
40
41 let oxi_rules_dir = project_dir.join(".oxi").join("rules");
42 if oxi_rules_dir.is_dir() {
43 rules.extend(scan_mdc_dir(&oxi_rules_dir, RuleSource::Project));
44 return rules;
45 }
46
47 for fallback in &[".cursorrules", ".clinerules"] {
49 let fallback_path = project_dir.join(fallback);
50 if fallback_path.is_dir() {
51 rules.extend(scan_mdc_dir(&fallback_path, RuleSource::Project));
52 } else if fallback_path.is_file() {
53 if let Ok(content) = std::fs::read_to_string(&fallback_path) {
54 if let Some(rule) =
55 builtin_rules::parse_rule_file(&content, fallback, RuleSource::Project)
56 {
57 rules.push(rule);
58 }
59 }
60 }
61 }
62
63 rules
64}
65
66fn scan_mdc_dir(dir: &Path, source: RuleSource) -> Vec<Rule> {
68 let mut rules = Vec::new();
69
70 let entries = match std::fs::read_dir(dir) {
71 Ok(entries) => entries,
72 Err(_) => return rules,
73 };
74
75 for entry in entries.flatten() {
76 let path = entry.path();
77 if path.extension().and_then(|e| e.to_str()) != Some("mdc") {
78 continue;
79 }
80
81 let name = match path.file_stem().and_then(|s| s.to_str()) {
82 Some(n) => n.to_string(),
83 None => continue,
84 };
85
86 let content = match std::fs::read_to_string(&path) {
87 Ok(c) => c,
88 Err(_) => continue,
89 };
90
91 if let Some(rule) = builtin_rules::parse_rule_file(&content, &name, source.clone()) {
92 rules.push(rule);
93 }
94 }
95
96 rules
97}
98
99pub struct StaticRuleRegistry {
104 rules: std::sync::RwLock<Vec<Rule>>,
105 injections: std::sync::RwLock<Vec<(String, u64)>>,
106}
107
108impl StaticRuleRegistry {
109 pub fn new(rules: Vec<Rule>) -> Self {
110 Self {
111 rules: std::sync::RwLock::new(rules),
112 injections: std::sync::RwLock::new(Vec::new()),
113 }
114 }
115}
116
117impl oxi_agent::agent_loop::ttsr::RuleRegistry for StaticRuleRegistry {
118 fn rules<'a>(
119 &'a self,
120 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Vec<Rule>> + Send + 'a>> {
121 let rules = self.rules.read().unwrap().clone();
122 Box::pin(std::future::ready(rules))
123 }
124
125 fn mark_injected(&self, name: &str, turn: u64) {
126 self.injections
127 .write()
128 .unwrap()
129 .push((name.to_string(), turn));
130 }
131
132 fn injected_records(&self) -> Vec<(String, u64)> {
133 self.injections.read().unwrap().clone()
134 }
135
136 fn restore(&self, records: Vec<(String, u64)>) {
137 *self.injections.write().unwrap() = records;
138 }
139}