1use crate::scanner::FilesystemType;
19use serde::{Deserialize, Serialize};
20use std::path::PathBuf;
21use std::time::SystemTime;
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub enum TraversalPath {
27 Direct,
29
30 Symlink {
32 target: PathBuf,
34 target_exists: bool,
36 },
37
38 Mount {
40 filesystem: FilesystemType,
42 mount_point: PathBuf,
44 },
45
46 Recursive {
48 depth: usize,
50 original: PathBuf,
52 },
53
54 Dependency {
56 manager: DependencyManager,
58 dep_root: PathBuf,
60 },
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
65pub enum DependencyManager {
66 Npm,
68 Cargo,
70 Python,
72 Go,
74 Ruby,
76 Composer,
78 Java,
80 Unknown,
82}
83
84impl DependencyManager {
85 pub fn dir_name(&self) -> &'static str {
87 match self {
88 Self::Npm => "node_modules",
89 Self::Cargo => "target",
90 Self::Python => ".venv",
91 Self::Go => "vendor",
92 Self::Ruby => "vendor",
93 Self::Composer => "vendor",
94 Self::Java => "build",
95 Self::Unknown => "",
96 }
97 }
98
99 pub fn from_dir_name(name: &str) -> Option<Self> {
101 match name {
102 "node_modules" => Some(Self::Npm),
103 "target" => Some(Self::Cargo),
104 ".venv" | "venv" | ".virtualenv" | "virtualenv" => Some(Self::Python),
105 "vendor" => Some(Self::Go), ".m2" | "build" | "out" => Some(Self::Java),
107 _ => None,
108 }
109 }
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct TraversalContext {
115 pub path: TraversalPath,
117
118 pub depth_from_root: usize,
120
121 pub in_git_worktree: bool,
123
124 pub in_submodule: bool,
126
127 pub parent_interest: Option<InterestLevel>,
129}
130
131impl Default for TraversalContext {
132 fn default() -> Self {
133 Self {
134 path: TraversalPath::Direct,
135 depth_from_root: 0,
136 in_git_worktree: false,
137 in_submodule: false,
138 parent_interest: None,
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
145pub enum InterestLevel {
146 Boring = 0,
148
149 #[default]
151 Background = 1,
152
153 Notable = 2,
155
156 Important = 3,
158
159 Critical = 4,
161}
162
163impl InterestLevel {
164 pub fn emoji(&self) -> &'static str {
166 match self {
167 Self::Boring => "đ¤",
168 Self::Background => "đĻ",
169 Self::Notable => "đ",
170 Self::Important => "đĨ",
171 Self::Critical => "â ī¸",
172 }
173 }
174
175 pub fn color(&self) -> &'static str {
177 match self {
178 Self::Boring => "bright_black",
179 Self::Background => "white",
180 Self::Notable => "cyan",
181 Self::Important => "yellow",
182 Self::Critical => "red",
183 }
184 }
185
186 pub fn from_score(score: f32) -> Self {
188 match score {
189 s if s >= 0.8 => Self::Critical,
190 s if s >= 0.6 => Self::Important,
191 s if s >= 0.4 => Self::Notable,
192 s if s >= 0.2 => Self::Background,
193 _ => Self::Boring,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
200pub enum RiskLevel {
201 None = 0,
203 Info = 1,
205 Low = 2,
207 Medium = 3,
209 High = 4,
211 Critical = 5,
213}
214
215impl RiskLevel {
216 pub fn emoji(&self) -> &'static str {
217 match self {
218 Self::None => "",
219 Self::Info => "âšī¸",
220 Self::Low => "đĩ",
221 Self::Medium => "đĄ",
222 Self::High => "đ ",
223 Self::Critical => "đ´",
224 }
225 }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub enum InterestFactor {
231 RecentlyModified {
233 hours_ago: f32,
235 weight: f32,
237 },
238
239 SecurityPattern {
241 risk: RiskLevel,
243 description: String,
245 weight: f32,
247 },
248
249 KeyProjectFile {
251 file_type: KeyFileType,
253 weight: f32,
255 },
256
257 ChangedSinceLastScan {
259 change: ChangeType,
261 weight: f32,
263 },
264
265 HotDirectory {
267 change_count: u32,
269 weight: f32,
271 },
272
273 SuspiciousDependency {
275 reason: String,
277 weight: f32,
279 },
280
281 GitStatus {
283 status: GitStatusType,
285 weight: f32,
287 },
288
289 Complexity {
291 description: String,
293 weight: f32,
295 },
296
297 InDependencyTree {
299 depth: usize,
301 weight: f32,
303 },
304
305 Custom {
307 name: String,
309 weight: f32,
311 },
312}
313
314impl InterestFactor {
315 pub fn weight(&self) -> f32 {
317 match self {
318 Self::RecentlyModified { weight, .. } => *weight,
319 Self::SecurityPattern { weight, .. } => *weight,
320 Self::KeyProjectFile { weight, .. } => *weight,
321 Self::ChangedSinceLastScan { weight, .. } => *weight,
322 Self::HotDirectory { weight, .. } => *weight,
323 Self::SuspiciousDependency { weight, .. } => *weight,
324 Self::GitStatus { weight, .. } => *weight,
325 Self::Complexity { weight, .. } => *weight,
326 Self::InDependencyTree { weight, .. } => *weight,
327 Self::Custom { weight, .. } => *weight,
328 }
329 }
330
331 pub fn description(&self) -> String {
333 match self {
334 Self::RecentlyModified { hours_ago, .. } => {
335 format!("Modified {:.1}h ago", hours_ago)
336 }
337 Self::SecurityPattern { description, .. } => description.clone(),
338 Self::KeyProjectFile { file_type, .. } => {
339 format!("Key file: {:?}", file_type)
340 }
341 Self::ChangedSinceLastScan { change, .. } => {
342 format!("Changed: {:?}", change)
343 }
344 Self::HotDirectory { change_count, .. } => {
345 format!("{} recent changes", change_count)
346 }
347 Self::SuspiciousDependency { reason, .. } => reason.clone(),
348 Self::GitStatus { status, .. } => format!("Git: {:?}", status),
349 Self::Complexity { description, .. } => description.clone(),
350 Self::InDependencyTree { depth, .. } => {
351 format!("Dependency depth: {}", depth)
352 }
353 Self::Custom { name, .. } => name.clone(),
354 }
355 }
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
360pub enum KeyFileType {
361 Documentation,
363 BuildConfig,
365 Configuration,
367 EntryPoint,
369 License,
371 CiConfig,
373 Container,
375 AiConfig,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
381pub enum ChangeType {
382 Added,
384 Modified,
386 Deleted,
388 PermissionChanged,
390 Renamed,
392 TypeChanged,
394}
395
396impl ChangeType {
397 pub fn emoji(&self) -> &'static str {
398 match self {
399 Self::Added => "+",
400 Self::Modified => "~",
401 Self::Deleted => "-",
402 Self::PermissionChanged => "đ",
403 Self::Renamed => "â",
404 Self::TypeChanged => "âĄ",
405 }
406 }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
411pub enum GitStatusType {
412 Uncommitted,
414 Conflict,
416 Staged,
418 Untracked,
420 Ahead,
422 Behind,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct InterestScore {
429 pub score: f32,
431
432 pub factors: Vec<InterestFactor>,
434
435 pub level: InterestLevel,
437
438 pub calculated_at: SystemTime,
440}
441
442impl InterestScore {
443 pub fn from_factors(factors: Vec<InterestFactor>) -> Self {
445 let score = factors.iter().map(|f| f.weight()).sum::<f32>().clamp(0.0, 1.0);
446 let level = InterestLevel::from_score(score);
447
448 Self {
449 score,
450 factors,
451 level,
452 calculated_at: SystemTime::now(),
453 }
454 }
455
456 pub fn boring() -> Self {
458 Self {
459 score: 0.0,
460 factors: vec![],
461 level: InterestLevel::Boring,
462 calculated_at: SystemTime::now(),
463 }
464 }
465
466 pub fn critical(reason: String) -> Self {
468 Self {
469 score: 1.0,
470 factors: vec![InterestFactor::SecurityPattern {
471 risk: RiskLevel::Critical,
472 description: reason,
473 weight: 1.0,
474 }],
475 level: InterestLevel::Critical,
476 calculated_at: SystemTime::now(),
477 }
478 }
479
480 pub fn should_show(&self) -> bool {
482 self.level >= InterestLevel::Notable
483 }
484
485 pub fn summary(&self) -> String {
487 if self.factors.is_empty() {
488 return String::from("No notable factors");
489 }
490
491 self.factors
492 .iter()
493 .map(|f| f.description())
494 .collect::<Vec<_>>()
495 .join(", ")
496 }
497}
498
499impl Default for InterestScore {
500 fn default() -> Self {
501 Self {
502 score: 0.1,
503 factors: vec![],
504 level: InterestLevel::Background,
505 calculated_at: SystemTime::now(),
506 }
507 }
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct InterestWeights {
513 pub recent_modification: f32,
515
516 pub security_critical: f32,
518
519 pub key_file: f32,
521
522 pub changed_since_scan: f32,
524
525 pub hot_directory: f32,
527
528 pub dependency_depth_penalty: f32,
530
531 pub git_changes: f32,
533}
534
535impl Default for InterestWeights {
536 fn default() -> Self {
537 Self {
538 recent_modification: 0.3,
539 security_critical: 1.0,
540 key_file: 0.5,
541 changed_since_scan: 0.4,
542 hot_directory: 0.3,
543 dependency_depth_penalty: -0.1,
544 git_changes: 0.35,
545 }
546 }
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn test_interest_level_from_score() {
555 assert_eq!(InterestLevel::from_score(0.0), InterestLevel::Boring);
556 assert_eq!(InterestLevel::from_score(0.1), InterestLevel::Boring);
557 assert_eq!(InterestLevel::from_score(0.3), InterestLevel::Background);
558 assert_eq!(InterestLevel::from_score(0.5), InterestLevel::Notable);
559 assert_eq!(InterestLevel::from_score(0.7), InterestLevel::Important);
560 assert_eq!(InterestLevel::from_score(0.9), InterestLevel::Critical);
561 assert_eq!(InterestLevel::from_score(1.0), InterestLevel::Critical);
562 }
563
564 #[test]
565 fn test_interest_score_from_factors() {
566 let factors = vec![
567 InterestFactor::RecentlyModified {
568 hours_ago: 2.0,
569 weight: 0.3,
570 },
571 InterestFactor::KeyProjectFile {
572 file_type: KeyFileType::BuildConfig,
573 weight: 0.5,
574 },
575 ];
576
577 let score = InterestScore::from_factors(factors);
578 assert!((score.score - 0.8).abs() < 0.01);
579 assert_eq!(score.level, InterestLevel::Critical);
580 }
581
582 #[test]
583 fn test_interest_score_clamping() {
584 let factors = vec![
585 InterestFactor::SecurityPattern {
586 risk: RiskLevel::Critical,
587 description: "Bad thing".to_string(),
588 weight: 1.0,
589 },
590 InterestFactor::HotDirectory {
591 change_count: 100,
592 weight: 0.5,
593 },
594 ];
595
596 let score = InterestScore::from_factors(factors);
597 assert_eq!(score.score, 1.0);
599 }
600
601 #[test]
602 fn test_dependency_manager_detection() {
603 assert_eq!(
604 DependencyManager::from_dir_name("node_modules"),
605 Some(DependencyManager::Npm)
606 );
607 assert_eq!(
608 DependencyManager::from_dir_name("target"),
609 Some(DependencyManager::Cargo)
610 );
611 assert_eq!(
612 DependencyManager::from_dir_name(".venv"),
613 Some(DependencyManager::Python)
614 );
615 assert_eq!(DependencyManager::from_dir_name("src"), None);
616 }
617}