ricecoder_learning/
scope_config.rs

1/// Scope configuration and isolation
2use 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/// Scope configuration with learning control flags
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ScopeConfiguration {
12    /// The scope this configuration applies to
13    pub scope: RuleScope,
14    /// Whether learning is enabled for this scope
15    pub learning_enabled: bool,
16    /// Whether to restrict learning to this scope only (project-only learning)
17    pub project_only: bool,
18    /// Whether approval is required for new rules
19    pub approval_required: bool,
20    /// Maximum number of rules to store in this scope
21    pub max_rules: usize,
22    /// Retention period in days
23    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    /// Create a new scope configuration
41    pub fn new(scope: RuleScope) -> Self {
42        Self {
43            scope,
44            ..Default::default()
45        }
46    }
47
48    /// Validate the configuration
49    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
66/// Scope configuration loader that handles project/user/default hierarchy
67pub struct ScopeConfigurationLoader;
68
69impl ScopeConfigurationLoader {
70    /// Load configuration from project/user/defaults hierarchy
71    pub async fn load_configuration(scope: RuleScope) -> Result<ScopeConfiguration> {
72        // Try to load from project config first
73        if let Ok(config) = Self::load_project_config(scope).await {
74            return Ok(config);
75        }
76
77        // Fall back to user config
78        if let Ok(config) = Self::load_user_config(scope).await {
79            return Ok(config);
80        }
81
82        // Fall back to defaults
83        Ok(ScopeConfiguration::new(scope))
84    }
85
86    /// Load configuration from project-level config file
87    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    /// Load configuration from user-level config file
108    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    /// Get the project-level config file path
129    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    /// Get the user-level config file path
138    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    /// Save configuration to project-level config file
149    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        // Ensure directory exists
155        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    /// Save configuration to user-level config file
172    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        // Ensure directory exists
178        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
195/// Scope filter for filtering rules by scope
196pub struct ScopeFilter;
197
198impl ScopeFilter {
199    /// Filter rules by scope
200    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    /// Filter rules by multiple scopes
205    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    /// Check if rules from different scopes interfere
214    pub fn check_scope_interference(
215        rules1: &[crate::models::Rule],
216        rules2: &[crate::models::Rule],
217    ) -> bool {
218        // Rules interfere if they have the same pattern but different actions
219        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    /// Get rules for a specific scope with precedence
230    pub fn get_rules_with_precedence(
231        rules: &[crate::models::Rule],
232        scope: RuleScope,
233    ) -> Vec<crate::models::Rule> {
234        // For project scope, include both project and session rules
235        // For global scope, include only global rules
236        // For session scope, include only session rules
237        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}