1use crate::error::{LearningError, Result};
3use crate::models::RuleScope;
4use ricecoder_storage::manager::PathResolver;
5use serde::{Deserialize, Serialize};
6use std::path::PathBuf;
7use tokio::fs;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ScopeConfiguration {
12 pub scope: RuleScope,
14 pub learning_enabled: bool,
16 pub project_only: bool,
18 pub approval_required: bool,
20 pub max_rules: usize,
22 pub retention_days: u32,
24}
25
26impl Default for ScopeConfiguration {
27 fn default() -> Self {
28 Self {
29 scope: RuleScope::Global,
30 learning_enabled: true,
31 project_only: false,
32 approval_required: false,
33 max_rules: 10000,
34 retention_days: 365,
35 }
36 }
37}
38
39impl ScopeConfiguration {
40 pub fn new(scope: RuleScope) -> Self {
42 Self {
43 scope,
44 ..Default::default()
45 }
46 }
47
48 pub fn validate(&self) -> Result<()> {
50 if self.max_rules == 0 {
51 return Err(LearningError::ConfigurationError(
52 "max_rules must be greater than 0".to_string(),
53 ));
54 }
55
56 if self.retention_days == 0 {
57 return Err(LearningError::ConfigurationError(
58 "retention_days must be greater than 0".to_string(),
59 ));
60 }
61
62 Ok(())
63 }
64}
65
66pub struct ScopeConfigurationLoader;
68
69impl ScopeConfigurationLoader {
70 pub async fn load_configuration(scope: RuleScope) -> Result<ScopeConfiguration> {
72 if let Ok(config) = Self::load_project_config(scope).await {
74 return Ok(config);
75 }
76
77 if let Ok(config) = Self::load_user_config(scope).await {
79 return Ok(config);
80 }
81
82 Ok(ScopeConfiguration::new(scope))
84 }
85
86 async fn load_project_config(scope: RuleScope) -> Result<ScopeConfiguration> {
88 let config_path = Self::get_project_config_path(scope)?;
89
90 if !config_path.exists() {
91 return Err(LearningError::ConfigurationError(
92 "Project config not found".to_string(),
93 ));
94 }
95
96 let content = fs::read_to_string(&config_path)
97 .await
98 .map_err(|e| LearningError::ConfigurationError(format!("Failed to read project config: {}", e)))?;
99
100 let config: ScopeConfiguration = serde_yaml::from_str(&content)
101 .map_err(|e| LearningError::ConfigurationError(format!("Failed to parse project config: {}", e)))?;
102
103 config.validate()?;
104 Ok(config)
105 }
106
107 async fn load_user_config(scope: RuleScope) -> Result<ScopeConfiguration> {
109 let config_path = Self::get_user_config_path(scope)?;
110
111 if !config_path.exists() {
112 return Err(LearningError::ConfigurationError(
113 "User config not found".to_string(),
114 ));
115 }
116
117 let content = fs::read_to_string(&config_path)
118 .await
119 .map_err(|e| LearningError::ConfigurationError(format!("Failed to read user config: {}", e)))?;
120
121 let config: ScopeConfiguration = serde_yaml::from_str(&content)
122 .map_err(|e| LearningError::ConfigurationError(format!("Failed to parse user config: {}", e)))?;
123
124 config.validate()?;
125 Ok(config)
126 }
127
128 fn get_project_config_path(scope: RuleScope) -> Result<PathBuf> {
130 match scope {
131 RuleScope::Global => Ok(PathBuf::from(".ricecoder/learning-global.yaml")),
132 RuleScope::Project => Ok(PathBuf::from(".ricecoder/learning-project.yaml")),
133 RuleScope::Session => Ok(PathBuf::from(".ricecoder/learning-session.yaml")),
134 }
135 }
136
137 fn get_user_config_path(scope: RuleScope) -> Result<PathBuf> {
139 let home = PathResolver::resolve_global_path()?;
140 let filename = match scope {
141 RuleScope::Global => "learning-global.yaml",
142 RuleScope::Project => "learning-project.yaml",
143 RuleScope::Session => "learning-session.yaml",
144 };
145 Ok(home.join(filename))
146 }
147
148 pub async fn save_project_config(config: &ScopeConfiguration) -> Result<()> {
150 config.validate()?;
151
152 let config_path = Self::get_project_config_path(config.scope)?;
153
154 if let Some(parent) = config_path.parent() {
156 fs::create_dir_all(parent)
157 .await
158 .map_err(|e| LearningError::ConfigurationError(format!("Failed to create config directory: {}", e)))?;
159 }
160
161 let yaml = serde_yaml::to_string(config)
162 .map_err(|e| LearningError::ConfigurationError(format!("Failed to serialize config: {}", e)))?;
163
164 fs::write(&config_path, yaml)
165 .await
166 .map_err(|e| LearningError::ConfigurationError(format!("Failed to write config file: {}", e)))?;
167
168 Ok(())
169 }
170
171 pub async fn save_user_config(config: &ScopeConfiguration) -> Result<()> {
173 config.validate()?;
174
175 let config_path = Self::get_user_config_path(config.scope)?;
176
177 if let Some(parent) = config_path.parent() {
179 fs::create_dir_all(parent)
180 .await
181 .map_err(|e| LearningError::ConfigurationError(format!("Failed to create config directory: {}", e)))?;
182 }
183
184 let yaml = serde_yaml::to_string(config)
185 .map_err(|e| LearningError::ConfigurationError(format!("Failed to serialize config: {}", e)))?;
186
187 fs::write(&config_path, yaml)
188 .await
189 .map_err(|e| LearningError::ConfigurationError(format!("Failed to write config file: {}", e)))?;
190
191 Ok(())
192 }
193}
194
195pub struct ScopeFilter;
197
198impl ScopeFilter {
199 pub fn filter_by_scope(rules: &[crate::models::Rule], scope: RuleScope) -> Vec<crate::models::Rule> {
201 rules.iter().filter(|r| r.scope == scope).cloned().collect()
202 }
203
204 pub fn filter_by_scopes(rules: &[crate::models::Rule], scopes: &[RuleScope]) -> Vec<crate::models::Rule> {
206 rules
207 .iter()
208 .filter(|r| scopes.contains(&r.scope))
209 .cloned()
210 .collect()
211 }
212
213 pub fn check_scope_interference(
215 rules1: &[crate::models::Rule],
216 rules2: &[crate::models::Rule],
217 ) -> bool {
218 for rule1 in rules1 {
220 for rule2 in rules2 {
221 if rule1.pattern == rule2.pattern && rule1.action != rule2.action {
222 return true;
223 }
224 }
225 }
226 false
227 }
228
229 pub fn get_rules_with_precedence(
231 rules: &[crate::models::Rule],
232 scope: RuleScope,
233 ) -> Vec<crate::models::Rule> {
234 match scope {
238 RuleScope::Project => {
239 Self::filter_by_scopes(rules, &[RuleScope::Project, RuleScope::Session])
240 }
241 RuleScope::Global => Self::filter_by_scope(rules, RuleScope::Global),
242 RuleScope::Session => Self::filter_by_scope(rules, RuleScope::Session),
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_scope_configuration_creation() {
253 let config = ScopeConfiguration::new(RuleScope::Global);
254 assert_eq!(config.scope, RuleScope::Global);
255 assert!(config.learning_enabled);
256 assert!(!config.project_only);
257 assert!(!config.approval_required);
258 }
259
260 #[test]
261 fn test_scope_configuration_validation() {
262 let mut config = ScopeConfiguration::new(RuleScope::Project);
263 assert!(config.validate().is_ok());
264
265 config.max_rules = 0;
266 assert!(config.validate().is_err());
267
268 config.max_rules = 100;
269 config.retention_days = 0;
270 assert!(config.validate().is_err());
271 }
272
273 #[test]
274 fn test_scope_configuration_default() {
275 let config = ScopeConfiguration::default();
276 assert_eq!(config.scope, RuleScope::Global);
277 assert!(config.learning_enabled);
278 assert_eq!(config.max_rules, 10000);
279 assert_eq!(config.retention_days, 365);
280 }
281
282 #[test]
283 fn test_scope_filter_by_scope() {
284 let rules = vec![
285 crate::models::Rule::new(
286 RuleScope::Global,
287 "pattern1".to_string(),
288 "action1".to_string(),
289 crate::models::RuleSource::Learned,
290 ),
291 crate::models::Rule::new(
292 RuleScope::Project,
293 "pattern2".to_string(),
294 "action2".to_string(),
295 crate::models::RuleSource::Learned,
296 ),
297 crate::models::Rule::new(
298 RuleScope::Global,
299 "pattern3".to_string(),
300 "action3".to_string(),
301 crate::models::RuleSource::Learned,
302 ),
303 ];
304
305 let global_rules = ScopeFilter::filter_by_scope(&rules, RuleScope::Global);
306 assert_eq!(global_rules.len(), 2);
307
308 let project_rules = ScopeFilter::filter_by_scope(&rules, RuleScope::Project);
309 assert_eq!(project_rules.len(), 1);
310 }
311
312 #[test]
313 fn test_scope_filter_by_scopes() {
314 let rules = vec![
315 crate::models::Rule::new(
316 RuleScope::Global,
317 "pattern1".to_string(),
318 "action1".to_string(),
319 crate::models::RuleSource::Learned,
320 ),
321 crate::models::Rule::new(
322 RuleScope::Project,
323 "pattern2".to_string(),
324 "action2".to_string(),
325 crate::models::RuleSource::Learned,
326 ),
327 crate::models::Rule::new(
328 RuleScope::Session,
329 "pattern3".to_string(),
330 "action3".to_string(),
331 crate::models::RuleSource::Learned,
332 ),
333 ];
334
335 let filtered = ScopeFilter::filter_by_scopes(&rules, &[RuleScope::Project, RuleScope::Session]);
336 assert_eq!(filtered.len(), 2);
337 }
338
339 #[test]
340 fn test_scope_interference_detection() {
341 let rules1 = vec![crate::models::Rule::new(
342 RuleScope::Global,
343 "pattern".to_string(),
344 "action1".to_string(),
345 crate::models::RuleSource::Learned,
346 )];
347
348 let rules2 = vec![crate::models::Rule::new(
349 RuleScope::Project,
350 "pattern".to_string(),
351 "action2".to_string(),
352 crate::models::RuleSource::Learned,
353 )];
354
355 assert!(ScopeFilter::check_scope_interference(&rules1, &rules2));
356 }
357
358 #[test]
359 fn test_scope_interference_no_conflict() {
360 let rules1 = vec![crate::models::Rule::new(
361 RuleScope::Global,
362 "pattern1".to_string(),
363 "action1".to_string(),
364 crate::models::RuleSource::Learned,
365 )];
366
367 let rules2 = vec![crate::models::Rule::new(
368 RuleScope::Project,
369 "pattern2".to_string(),
370 "action2".to_string(),
371 crate::models::RuleSource::Learned,
372 )];
373
374 assert!(!ScopeFilter::check_scope_interference(&rules1, &rules2));
375 }
376
377 #[test]
378 fn test_get_rules_with_precedence_project() {
379 let rules = vec![
380 crate::models::Rule::new(
381 RuleScope::Global,
382 "pattern1".to_string(),
383 "action1".to_string(),
384 crate::models::RuleSource::Learned,
385 ),
386 crate::models::Rule::new(
387 RuleScope::Project,
388 "pattern2".to_string(),
389 "action2".to_string(),
390 crate::models::RuleSource::Learned,
391 ),
392 crate::models::Rule::new(
393 RuleScope::Session,
394 "pattern3".to_string(),
395 "action3".to_string(),
396 crate::models::RuleSource::Learned,
397 ),
398 ];
399
400 let filtered = ScopeFilter::get_rules_with_precedence(&rules, RuleScope::Project);
401 assert_eq!(filtered.len(), 2);
402 assert!(filtered.iter().all(|r| r.scope == RuleScope::Project || r.scope == RuleScope::Session));
403 }
404
405 #[test]
406 fn test_get_rules_with_precedence_global() {
407 let rules = vec![
408 crate::models::Rule::new(
409 RuleScope::Global,
410 "pattern1".to_string(),
411 "action1".to_string(),
412 crate::models::RuleSource::Learned,
413 ),
414 crate::models::Rule::new(
415 RuleScope::Project,
416 "pattern2".to_string(),
417 "action2".to_string(),
418 crate::models::RuleSource::Learned,
419 ),
420 crate::models::Rule::new(
421 RuleScope::Session,
422 "pattern3".to_string(),
423 "action3".to_string(),
424 crate::models::RuleSource::Learned,
425 ),
426 ];
427
428 let filtered = ScopeFilter::get_rules_with_precedence(&rules, RuleScope::Global);
429 assert_eq!(filtered.len(), 1);
430 assert!(filtered.iter().all(|r| r.scope == RuleScope::Global));
431 }
432
433 #[test]
434 fn test_scope_configuration_loading_defaults() {
435 let config = ScopeConfiguration::new(RuleScope::Project);
436 assert_eq!(config.scope, RuleScope::Project);
437 assert!(config.learning_enabled);
438 assert!(!config.project_only);
439 assert!(!config.approval_required);
440 assert_eq!(config.max_rules, 10000);
441 assert_eq!(config.retention_days, 365);
442 }
443
444 #[test]
445 fn test_scope_configuration_project_only_flag() {
446 let mut config = ScopeConfiguration::new(RuleScope::Project);
447 assert!(!config.project_only);
448
449 config.project_only = true;
450 assert!(config.project_only);
451 assert!(config.validate().is_ok());
452 }
453
454 #[test]
455 fn test_scope_configuration_approval_required_flag() {
456 let mut config = ScopeConfiguration::new(RuleScope::Global);
457 assert!(!config.approval_required);
458
459 config.approval_required = true;
460 assert!(config.approval_required);
461 assert!(config.validate().is_ok());
462 }
463
464 #[test]
465 fn test_scope_configuration_learning_enabled_flag() {
466 let mut config = ScopeConfiguration::new(RuleScope::Session);
467 assert!(config.learning_enabled);
468
469 config.learning_enabled = false;
470 assert!(!config.learning_enabled);
471 assert!(config.validate().is_ok());
472 }
473
474 #[test]
475 fn test_scope_configuration_max_rules_validation() {
476 let mut config = ScopeConfiguration::new(RuleScope::Global);
477 config.max_rules = 100;
478 assert!(config.validate().is_ok());
479
480 config.max_rules = 0;
481 assert!(config.validate().is_err());
482
483 config.max_rules = 1;
484 assert!(config.validate().is_ok());
485 }
486
487 #[test]
488 fn test_scope_configuration_retention_days_validation() {
489 let mut config = ScopeConfiguration::new(RuleScope::Global);
490 config.retention_days = 30;
491 assert!(config.validate().is_ok());
492
493 config.retention_days = 0;
494 assert!(config.validate().is_err());
495
496 config.retention_days = 1;
497 assert!(config.validate().is_ok());
498 }
499
500 #[test]
501 fn test_scope_filter_empty_rules() {
502 let rules: Vec<crate::models::Rule> = Vec::new();
503
504 let global_rules = ScopeFilter::filter_by_scope(&rules, RuleScope::Global);
505 assert_eq!(global_rules.len(), 0);
506
507 let project_rules = ScopeFilter::filter_by_scope(&rules, RuleScope::Project);
508 assert_eq!(project_rules.len(), 0);
509
510 let session_rules = ScopeFilter::filter_by_scope(&rules, RuleScope::Session);
511 assert_eq!(session_rules.len(), 0);
512 }
513
514 #[test]
515 fn test_scope_filter_single_scope() {
516 let rules = vec![
517 crate::models::Rule::new(
518 RuleScope::Global,
519 "pattern1".to_string(),
520 "action1".to_string(),
521 crate::models::RuleSource::Learned,
522 ),
523 crate::models::Rule::new(
524 RuleScope::Global,
525 "pattern2".to_string(),
526 "action2".to_string(),
527 crate::models::RuleSource::Learned,
528 ),
529 ];
530
531 let global_rules = ScopeFilter::filter_by_scope(&rules, RuleScope::Global);
532 assert_eq!(global_rules.len(), 2);
533
534 let project_rules = ScopeFilter::filter_by_scope(&rules, RuleScope::Project);
535 assert_eq!(project_rules.len(), 0);
536 }
537
538 #[test]
539 fn test_scope_filter_multiple_scopes_combination() {
540 let rules = vec![
541 crate::models::Rule::new(
542 RuleScope::Global,
543 "pattern1".to_string(),
544 "action1".to_string(),
545 crate::models::RuleSource::Learned,
546 ),
547 crate::models::Rule::new(
548 RuleScope::Project,
549 "pattern2".to_string(),
550 "action2".to_string(),
551 crate::models::RuleSource::Learned,
552 ),
553 crate::models::Rule::new(
554 RuleScope::Session,
555 "pattern3".to_string(),
556 "action3".to_string(),
557 crate::models::RuleSource::Learned,
558 ),
559 crate::models::Rule::new(
560 RuleScope::Project,
561 "pattern4".to_string(),
562 "action4".to_string(),
563 crate::models::RuleSource::Learned,
564 ),
565 ];
566
567 let filtered = ScopeFilter::filter_by_scopes(&rules, &[RuleScope::Project, RuleScope::Session]);
568 assert_eq!(filtered.len(), 3);
569 assert!(filtered.iter().all(|r| r.scope == RuleScope::Project || r.scope == RuleScope::Session));
570 }
571
572 #[test]
573 fn test_scope_interference_same_pattern_different_action() {
574 let rules1 = vec![crate::models::Rule::new(
575 RuleScope::Global,
576 "pattern".to_string(),
577 "action1".to_string(),
578 crate::models::RuleSource::Learned,
579 )];
580
581 let rules2 = vec![crate::models::Rule::new(
582 RuleScope::Project,
583 "pattern".to_string(),
584 "action2".to_string(),
585 crate::models::RuleSource::Learned,
586 )];
587
588 assert!(ScopeFilter::check_scope_interference(&rules1, &rules2));
589 }
590
591 #[test]
592 fn test_scope_interference_same_pattern_same_action() {
593 let rules1 = vec![crate::models::Rule::new(
594 RuleScope::Global,
595 "pattern".to_string(),
596 "action".to_string(),
597 crate::models::RuleSource::Learned,
598 )];
599
600 let rules2 = vec![crate::models::Rule::new(
601 RuleScope::Project,
602 "pattern".to_string(),
603 "action".to_string(),
604 crate::models::RuleSource::Learned,
605 )];
606
607 assert!(!ScopeFilter::check_scope_interference(&rules1, &rules2));
608 }
609
610 #[test]
611 fn test_scope_interference_multiple_rules() {
612 let rules1 = vec![
613 crate::models::Rule::new(
614 RuleScope::Global,
615 "pattern1".to_string(),
616 "action1".to_string(),
617 crate::models::RuleSource::Learned,
618 ),
619 crate::models::Rule::new(
620 RuleScope::Global,
621 "pattern2".to_string(),
622 "action2".to_string(),
623 crate::models::RuleSource::Learned,
624 ),
625 ];
626
627 let rules2 = vec![
628 crate::models::Rule::new(
629 RuleScope::Project,
630 "pattern1".to_string(),
631 "action_different".to_string(),
632 crate::models::RuleSource::Learned,
633 ),
634 crate::models::Rule::new(
635 RuleScope::Project,
636 "pattern3".to_string(),
637 "action3".to_string(),
638 crate::models::RuleSource::Learned,
639 ),
640 ];
641
642 assert!(ScopeFilter::check_scope_interference(&rules1, &rules2));
643 }
644
645 #[test]
646 fn test_scope_precedence_project_includes_session() {
647 let rules = vec![
648 crate::models::Rule::new(
649 RuleScope::Global,
650 "pattern1".to_string(),
651 "action1".to_string(),
652 crate::models::RuleSource::Learned,
653 ),
654 crate::models::Rule::new(
655 RuleScope::Project,
656 "pattern2".to_string(),
657 "action2".to_string(),
658 crate::models::RuleSource::Learned,
659 ),
660 crate::models::Rule::new(
661 RuleScope::Session,
662 "pattern3".to_string(),
663 "action3".to_string(),
664 crate::models::RuleSource::Learned,
665 ),
666 ];
667
668 let project_rules = ScopeFilter::get_rules_with_precedence(&rules, RuleScope::Project);
669 assert_eq!(project_rules.len(), 2);
670 assert!(project_rules.iter().any(|r| r.scope == RuleScope::Project));
671 assert!(project_rules.iter().any(|r| r.scope == RuleScope::Session));
672 assert!(!project_rules.iter().any(|r| r.scope == RuleScope::Global));
673 }
674
675 #[test]
676 fn test_scope_precedence_session_only_session() {
677 let rules = vec![
678 crate::models::Rule::new(
679 RuleScope::Global,
680 "pattern1".to_string(),
681 "action1".to_string(),
682 crate::models::RuleSource::Learned,
683 ),
684 crate::models::Rule::new(
685 RuleScope::Project,
686 "pattern2".to_string(),
687 "action2".to_string(),
688 crate::models::RuleSource::Learned,
689 ),
690 crate::models::Rule::new(
691 RuleScope::Session,
692 "pattern3".to_string(),
693 "action3".to_string(),
694 crate::models::RuleSource::Learned,
695 ),
696 ];
697
698 let session_rules = ScopeFilter::get_rules_with_precedence(&rules, RuleScope::Session);
699 assert_eq!(session_rules.len(), 1);
700 assert!(session_rules.iter().all(|r| r.scope == RuleScope::Session));
701 }
702}