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 && let Ok(content) = std::fs::read_to_string(&fallback_path)
54 && let Some(rule) =
55 builtin_rules::parse_rule_file(&content, fallback, RuleSource::Project)
56 {
57 rules.push(rule);
58 }
59 }
60
61 rules
62}
63
64fn scan_mdc_dir(dir: &Path, source: RuleSource) -> Vec<Rule> {
66 let mut rules = Vec::new();
67
68 let entries = match std::fs::read_dir(dir) {
69 Ok(entries) => entries,
70 Err(_) => return rules,
71 };
72
73 for entry in entries.flatten() {
74 let path = entry.path();
75 if path.extension().and_then(|e| e.to_str()) != Some("mdc") {
76 continue;
77 }
78
79 let name = match path.file_stem().and_then(|s| s.to_str()) {
80 Some(n) => n.to_string(),
81 None => continue,
82 };
83
84 let content = match std::fs::read_to_string(&path) {
85 Ok(c) => c,
86 Err(_) => continue,
87 };
88
89 if let Some(rule) = builtin_rules::parse_rule_file(&content, &name, source.clone()) {
90 rules.push(rule);
91 }
92 }
93
94 rules
95}
96
97pub struct StaticRuleRegistry {
102 rules: std::sync::RwLock<Vec<Rule>>,
103 injections: std::sync::RwLock<Vec<(String, u64)>>,
104}
105
106impl StaticRuleRegistry {
107 pub fn new(rules: Vec<Rule>) -> Self {
109 Self {
110 rules: std::sync::RwLock::new(rules),
111 injections: std::sync::RwLock::new(Vec::new()),
112 }
113 }
114}
115
116impl oxi_agent::agent_loop::ttsr::RuleRegistry for StaticRuleRegistry {
117 fn rules<'a>(
118 &'a self,
119 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Vec<Rule>> + Send + 'a>> {
120 let rules = self
121 .rules
122 .read()
123 .unwrap_or_else(std::sync::PoisonError::into_inner)
124 .clone();
125 Box::pin(std::future::ready(rules))
126 }
127
128 fn mark_injected(&self, name: &str, turn: u64) {
129 self.injections
130 .write()
131 .unwrap_or_else(std::sync::PoisonError::into_inner)
132 .push((name.to_string(), turn));
133 }
134
135 fn injected_records(&self) -> Vec<(String, u64)> {
136 self.injections
137 .read()
138 .unwrap_or_else(std::sync::PoisonError::into_inner)
139 .clone()
140 }
141
142 fn restore(&self, records: Vec<(String, u64)>) {
143 *self
144 .injections
145 .write()
146 .unwrap_or_else(std::sync::PoisonError::into_inner) = records;
147 }
148}