oxios_kernel/project/
detection.rs1use std::path::PathBuf;
11
12#[cfg(test)]
13use super::ProjectSource;
14use super::{Project, ProjectId};
15
16#[derive(Debug)]
18pub enum DetectionResult {
19 Found(ProjectId),
21 NoMatch { detected_path: Option<PathBuf> },
23}
24
25pub fn detect_project(message: &str, projects: &[Project]) -> DetectionResult {
32 let lower = message.to_lowercase();
34 for project in projects {
35 if lower.contains(&project.name.to_lowercase()) {
36 return DetectionResult::Found(project.id);
37 }
38 }
39
40 if let Some(path) = extract_path(message) {
42 for project in projects {
43 if project
44 .paths
45 .iter()
46 .any(|p| path.starts_with(p) || p.starts_with(&path))
47 {
48 return DetectionResult::Found(project.id);
49 }
50 }
51 return DetectionResult::NoMatch {
52 detected_path: Some(path),
53 };
54 }
55
56 for project in projects {
58 for tag in &project.tags {
59 if lower.contains(&tag.to_lowercase()) {
60 return DetectionResult::Found(project.id);
61 }
62 }
63 }
64
65 DetectionResult::NoMatch {
66 detected_path: None,
67 }
68}
69
70pub fn extract_path(message: &str) -> Option<PathBuf> {
74 for word in message.split_whitespace() {
76 let cleaned = word.trim_matches(|c: char| {
77 !c.is_alphanumeric() && c != '/' && c != '.' && c != '-' && c != '_'
78 });
79 if cleaned.starts_with('/') && cleaned.len() > 2 {
80 let path = PathBuf::from(cleaned);
81 if path.parent().is_some() {
83 return Some(path);
84 }
85 }
86 }
87
88 for word in message.split_whitespace() {
90 let cleaned = word.trim_matches(|c: char| {
91 !c.is_alphanumeric() && c != '/' && c != '.' && c != '-' && c != '_' && c != '~'
92 });
93 if cleaned.starts_with("~/") && cleaned.len() > 2 {
94 if let Some(home) = std::env::var_os("HOME") {
95 let expanded = cleaned.replacen("~", &home.to_string_lossy(), 1);
96 return Some(PathBuf::from(expanded));
97 }
98 }
99 }
100
101 None
102}
103
104pub fn find_by_id(projects: &[Project], id: ProjectId) -> Option<&Project> {
106 projects.iter().find(|p| p.id == id)
107}
108
109pub fn find_by_name<'a>(projects: &'a [Project], name: &str) -> Option<&'a Project> {
111 let lower = name.to_lowercase();
112 projects.iter().find(|p| p.name.to_lowercase() == lower)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn make_projects() -> Vec<Project> {
120 let mut oxios = Project::new("oxios", ProjectSource::Manual);
121 oxios
122 .paths
123 .push(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
124 oxios.add_tag("agent-os");
125
126 let mut oxi = Project::new("oxi", ProjectSource::Manual);
127 oxi.paths
128 .push(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxi"));
129 oxi.add_tag("sdk");
130
131 let mut blog = Project::new("my-blog", ProjectSource::Manual);
132 blog.add_tag("writing");
133 blog.add_tag("content");
134
135 vec![oxios, oxi, blog]
136 }
137
138 #[test]
139 fn test_detect_by_name() {
140 let projects = make_projects();
141 let result = detect_project("oxios 코드리뷰해줘", &projects);
142 assert!(matches!(result, DetectionResult::Found(id) if id == projects[0].id));
143 }
144
145 #[test]
146 fn test_detect_by_path() {
147 let projects = make_projects();
148 let result = detect_project("/Volumes/MERCURY/PROJECTS/oxios에서 작업", &projects);
149 assert!(matches!(result, DetectionResult::Found(id) if id == projects[0].id));
150 }
151
152 #[test]
153 fn test_detect_by_tag() {
154 let projects = make_projects();
155 let result = detect_project("writing 관련 도움이 필요해", &projects);
156 assert!(matches!(result, DetectionResult::Found(id) if id == projects[2].id));
157 }
158
159 #[test]
160 fn test_detect_no_match_with_path() {
161 let projects = make_projects();
162 let result = detect_project("/Volumes/MERCURY/PROJECTS/unknown 에서 작업", &projects);
163 assert!(matches!(
164 result,
165 DetectionResult::NoMatch {
166 detected_path: Some(_)
167 }
168 ));
169 }
170
171 #[test]
172 fn test_detect_no_match() {
173 let projects = make_projects();
174 let result = detect_project("오늘 점심 뭐 먹지?", &projects);
175 assert!(matches!(
176 result,
177 DetectionResult::NoMatch {
178 detected_path: None
179 }
180 ));
181 }
182
183 #[test]
184 fn test_extract_path() {
185 assert_eq!(
186 extract_path("/Volumes/MERCURY/PROJECTS/oxios"),
187 Some(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"))
188 );
189 assert_eq!(extract_path("no path here"), None);
190 }
191
192 #[test]
193 fn test_find_by_name() {
194 let projects = make_projects();
195 assert!(find_by_name(&projects, "oxios").is_some());
196 assert!(find_by_name(&projects, "Oxios").is_some()); assert!(find_by_name(&projects, "nonexistent").is_none());
198 }
199}