ricecoder_teams/
config.rs

1/// Team configuration management
2use crate::error::{Result, TeamError};
3use crate::models::{MergedStandards, StandardsOverride, TeamStandards};
4use chrono::Utc;
5use ricecoder_storage::PathResolver;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12/// Change history entry for tracking modifications
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ChangeHistoryEntry {
15    pub timestamp: chrono::DateTime<Utc>,
16    pub version: u32,
17    pub description: String,
18    pub changed_by: String,
19}
20
21/// Manages team-level configuration storage and inheritance
22pub struct TeamConfigManager {
23    /// Cache for standards to improve performance
24    standards_cache: Arc<RwLock<HashMap<String, TeamStandards>>>,
25    /// Change history for tracking modifications
26    change_history: Arc<RwLock<HashMap<String, Vec<ChangeHistoryEntry>>>>,
27}
28
29impl TeamConfigManager {
30    /// Create a new TeamConfigManager
31    pub fn new() -> Self {
32        TeamConfigManager {
33            standards_cache: Arc::new(RwLock::new(HashMap::new())),
34            change_history: Arc::new(RwLock::new(HashMap::new())),
35        }
36    }
37
38    /// Store standards for a team using ricecoder-storage in YAML format
39    pub async fn store_standards(&self, team_id: &str, standards: TeamStandards) -> Result<()> {
40        // Resolve the storage path for team standards
41        let storage_path = Self::resolve_team_standards_path(team_id)?;
42
43        // Ensure parent directory exists
44        if let Some(parent) = storage_path.parent() {
45            std::fs::create_dir_all(parent).map_err(|e| {
46                TeamError::StorageError(format!("Failed to create storage directory: {}", e))
47            })?;
48        }
49
50        // Serialize standards to YAML
51        let yaml_content = serde_yaml::to_string(&standards).map_err(TeamError::YamlError)?;
52
53        // Write to file
54        std::fs::write(&storage_path, yaml_content).map_err(|e| {
55            TeamError::StorageError(format!("Failed to write standards file: {}", e))
56        })?;
57
58        // Update cache
59        let mut cache = self.standards_cache.write().await;
60        cache.insert(team_id.to_string(), standards);
61
62        tracing::info!(
63            team_id = %team_id,
64            path = ?storage_path,
65            "Team standards stored successfully"
66        );
67
68        Ok(())
69    }
70
71    /// Retrieve standards for a team using PathResolver with caching
72    pub async fn get_standards(&self, team_id: &str) -> Result<TeamStandards> {
73        // Check cache first
74        {
75            let cache = self.standards_cache.read().await;
76            if let Some(standards) = cache.get(team_id) {
77                tracing::debug!(team_id = %team_id, "Retrieved standards from cache");
78                return Ok(standards.clone());
79            }
80        }
81
82        // Load from storage
83        let storage_path = Self::resolve_team_standards_path(team_id)?;
84
85        if !storage_path.exists() {
86            return Err(TeamError::TeamNotFound(format!(
87                "Standards not found for team: {}",
88                team_id
89            )));
90        }
91
92        let yaml_content = std::fs::read_to_string(&storage_path).map_err(|e| {
93            TeamError::StorageError(format!("Failed to read standards file: {}", e))
94        })?;
95
96        let standards: TeamStandards =
97            serde_yaml::from_str(&yaml_content).map_err(TeamError::YamlError)?;
98
99        // Update cache
100        let mut cache = self.standards_cache.write().await;
101        cache.insert(team_id.to_string(), standards.clone());
102
103        tracing::info!(team_id = %team_id, "Retrieved team standards from storage");
104
105        Ok(standards)
106    }
107
108    /// Apply hierarchy: Organization → Team → Project using ConfigMerger
109    pub async fn apply_hierarchy(
110        &self,
111        org_id: &str,
112        team_id: &str,
113        project_id: &str,
114    ) -> Result<MergedStandards> {
115        // Load standards from each level
116        let org_standards = self.get_standards(org_id).await.ok();
117        let team_standards = self.get_standards(team_id).await.ok();
118        let project_standards = self.get_standards(project_id).await.ok();
119
120        // Merge standards with hierarchy: Organization → Team → Project
121        // Project-level standards override team-level, which override organization-level
122        let final_standards = Self::merge_standards_hierarchy(
123            org_standards.clone(),
124            team_standards.clone(),
125            project_standards.clone(),
126        )?;
127
128        tracing::info!(
129            org_id = %org_id,
130            team_id = %team_id,
131            project_id = %project_id,
132            "Standards hierarchy applied successfully"
133        );
134
135        Ok(MergedStandards {
136            organization_standards: org_standards,
137            team_standards,
138            project_standards,
139            final_standards,
140        })
141    }
142
143    /// Override standards at project level with validation
144    pub async fn override_standards(
145        &self,
146        project_id: &str,
147        overrides: StandardsOverride,
148    ) -> Result<()> {
149        // Load current project standards
150        let mut project_standards = self.get_standards(project_id).await?;
151
152        // Validate overrides
153        Self::validate_overrides(&project_standards, &overrides)?;
154
155        // Apply overrides
156        for override_id in &overrides.overridden_standards {
157            // Remove overridden rules
158            project_standards
159                .code_review_rules
160                .retain(|r| &r.id != override_id);
161        }
162
163        // Update version
164        project_standards.version += 1;
165        project_standards.updated_at = Utc::now();
166
167        // Store updated standards
168        self.store_standards(project_id, project_standards).await?;
169
170        // Track the change
171        self.track_changes(
172            project_id,
173            &format!("Applied {} overrides", overrides.overridden_standards.len()),
174        )
175        .await?;
176
177        tracing::info!(
178            project_id = %project_id,
179            override_count = %overrides.overridden_standards.len(),
180            "Standards overrides applied successfully"
181        );
182
183        Ok(())
184    }
185
186    /// Track changes to standards with timestamps and version identifiers
187    pub async fn track_changes(&self, team_id: &str, change_description: &str) -> Result<()> {
188        let entry = ChangeHistoryEntry {
189            timestamp: Utc::now(),
190            version: 1, // TODO: Get actual version from standards
191            description: change_description.to_string(),
192            changed_by: "system".to_string(), // TODO: Get actual user
193        };
194
195        let mut history = self.change_history.write().await;
196        history
197            .entry(team_id.to_string())
198            .or_insert_with(Vec::new)
199            .push(entry);
200
201        tracing::info!(
202            team_id = %team_id,
203            change = %change_description,
204            "Standards change tracked successfully"
205        );
206
207        Ok(())
208    }
209
210    /// Get change history for a team
211    pub async fn get_change_history(&self, team_id: &str) -> Result<Vec<ChangeHistoryEntry>> {
212        let history = self.change_history.read().await;
213        Ok(history.get(team_id).cloned().unwrap_or_default())
214    }
215
216    // Helper functions
217
218    /// Resolve the storage path for team standards
219    fn resolve_team_standards_path(team_id: &str) -> Result<PathBuf> {
220        let global_path = PathResolver::resolve_global_path()
221            .map_err(|e| TeamError::StorageError(e.to_string()))?;
222
223        let standards_path = global_path
224            .join("teams")
225            .join(team_id)
226            .join("standards.yaml");
227
228        Ok(standards_path)
229    }
230
231    /// Merge standards from hierarchy with project overrides taking precedence
232    pub fn merge_standards_hierarchy(
233        org_standards: Option<TeamStandards>,
234        team_standards: Option<TeamStandards>,
235        project_standards: Option<TeamStandards>,
236    ) -> Result<TeamStandards> {
237        // Start with organization standards as base
238        let mut merged = org_standards.unwrap_or_else(|| TeamStandards {
239            id: "merged".to_string(),
240            team_id: "merged".to_string(),
241            code_review_rules: Vec::new(),
242            templates: Vec::new(),
243            steering_docs: Vec::new(),
244            compliance_requirements: Vec::new(),
245            version: 1,
246            created_at: Utc::now(),
247            updated_at: Utc::now(),
248        });
249
250        // Merge team standards (override organization)
251        if let Some(team) = team_standards {
252            merged.code_review_rules.extend(team.code_review_rules);
253            merged.templates.extend(team.templates);
254            merged.steering_docs.extend(team.steering_docs);
255            merged
256                .compliance_requirements
257                .extend(team.compliance_requirements);
258            merged.version = team.version;
259            merged.updated_at = team.updated_at;
260        }
261
262        // Merge project standards (override team and organization)
263        if let Some(project) = project_standards {
264            merged.code_review_rules.extend(project.code_review_rules);
265            merged.templates.extend(project.templates);
266            merged.steering_docs.extend(project.steering_docs);
267            merged
268                .compliance_requirements
269                .extend(project.compliance_requirements);
270            merged.version = project.version;
271            merged.updated_at = project.updated_at;
272        }
273
274        merged.updated_at = Utc::now();
275
276        Ok(merged)
277    }
278
279    /// Validate overrides before applying
280    fn validate_overrides(standards: &TeamStandards, overrides: &StandardsOverride) -> Result<()> {
281        for override_id in &overrides.overridden_standards {
282            let exists = standards
283                .code_review_rules
284                .iter()
285                .any(|r| &r.id == override_id);
286
287            if !exists {
288                return Err(TeamError::ConfigError(format!(
289                    "Override target not found: {}",
290                    override_id
291                )));
292            }
293        }
294
295        Ok(())
296    }
297}
298
299impl Default for TeamConfigManager {
300    fn default() -> Self {
301        Self::new()
302    }
303}