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("~/")
94 && cleaned.len() > 2
95 && let Some(home) = std::env::var_os("HOME")
96 {
97 let expanded = cleaned.replacen("~", &home.to_string_lossy(), 1);
98 return Some(PathBuf::from(expanded));
99 }
100 }
101
102 None
103}
104
105pub fn find_by_id(projects: &[Project], id: ProjectId) -> Option<&Project> {
107 projects.iter().find(|p| p.id == id)
108}
109
110pub fn find_by_name<'a>(projects: &'a [Project], name: &str) -> Option<&'a Project> {
112 let lower = name.to_lowercase();
113 projects.iter().find(|p| p.name.to_lowercase() == lower)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn make_projects() -> Vec<Project> {
121 let mut oxios = Project::new("oxios", ProjectSource::Manual);
122 oxios
123 .paths
124 .push(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"));
125 oxios.add_tag("agent-os");
126
127 let mut oxi = Project::new("oxi", ProjectSource::Manual);
128 oxi.paths
129 .push(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxi"));
130 oxi.add_tag("sdk");
131
132 let mut blog = Project::new("my-blog", ProjectSource::Manual);
133 blog.add_tag("writing");
134 blog.add_tag("content");
135
136 vec![oxios, oxi, blog]
137 }
138
139 #[test]
140 fn test_detect_by_name() {
141 let projects = make_projects();
142 let result = detect_project("oxios 코드리뷰해줘", &projects);
143 assert!(matches!(result, DetectionResult::Found(id) if id == projects[0].id));
144 }
145
146 #[test]
147 fn test_detect_by_path() {
148 let projects = make_projects();
149 let result = detect_project("/Volumes/MERCURY/PROJECTS/oxios에서 작업", &projects);
150 assert!(matches!(result, DetectionResult::Found(id) if id == projects[0].id));
151 }
152
153 #[test]
154 fn test_detect_by_tag() {
155 let projects = make_projects();
156 let result = detect_project("writing 관련 도움이 필요해", &projects);
157 assert!(matches!(result, DetectionResult::Found(id) if id == projects[2].id));
158 }
159
160 #[test]
161 fn test_detect_no_match_with_path() {
162 let projects = make_projects();
163 let result = detect_project("/Volumes/MERCURY/PROJECTS/unknown 에서 작업", &projects);
164 assert!(matches!(
165 result,
166 DetectionResult::NoMatch {
167 detected_path: Some(_)
168 }
169 ));
170 }
171
172 #[test]
173 fn test_detect_no_match() {
174 let projects = make_projects();
175 let result = detect_project("오늘 점심 뭐 먹지?", &projects);
176 assert!(matches!(
177 result,
178 DetectionResult::NoMatch {
179 detected_path: None
180 }
181 ));
182 }
183
184 #[test]
185 fn test_extract_path() {
186 assert_eq!(
187 extract_path("/Volumes/MERCURY/PROJECTS/oxios"),
188 Some(PathBuf::from("/Volumes/MERCURY/PROJECTS/oxios"))
189 );
190 assert_eq!(extract_path("no path here"), None);
191 }
192
193 #[test]
194 fn test_find_by_name() {
195 let projects = make_projects();
196 assert!(find_by_name(&projects, "oxios").is_some());
197 assert!(find_by_name(&projects, "Oxios").is_some()); assert!(find_by_name(&projects, "nonexistent").is_none());
199 }
200}