Skip to main content

omni_dev/claude/context/
files.rs

1//! File-based context analysis and architectural understanding
2
3use crate::data::context::{
4    ArchitecturalLayer, ChangeImpact, FileContext, FilePurpose, ProjectSignificance,
5};
6use std::path::{Path, PathBuf};
7
8/// File context analyzer
9pub struct FileAnalyzer;
10
11impl FileAnalyzer {
12    /// Analyze a file and determine its context within the project
13    pub fn analyze_file(path: &Path, change_type: &str) -> FileContext {
14        let file_purpose = determine_file_purpose(path);
15        let architectural_layer = determine_architectural_layer(path, &file_purpose);
16        let change_impact = determine_change_impact(change_type, &file_purpose);
17        let project_significance = determine_project_significance(path, &file_purpose);
18
19        FileContext {
20            path: path.to_path_buf(),
21            file_purpose,
22            architectural_layer,
23            change_impact,
24            project_significance,
25        }
26    }
27
28    /// Analyze multiple files to understand the scope of changes
29    pub fn analyze_file_set(files: &[(PathBuf, String)]) -> Vec<FileContext> {
30        files
31            .iter()
32            .map(|(path, change_type)| Self::analyze_file(path, change_type))
33            .collect()
34    }
35
36    /// Determine the primary architectural impact of a set of file changes
37    pub fn primary_architectural_impact(contexts: &[FileContext]) -> ArchitecturalLayer {
38        use std::collections::HashMap;
39
40        let mut layer_counts = HashMap::new();
41        for context in contexts {
42            *layer_counts
43                .entry(context.architectural_layer.clone())
44                .or_insert(0) += 1;
45        }
46
47        // Return the most common architectural layer, with precedence for critical layers
48        layer_counts
49            .into_iter()
50            .max_by_key(|(layer, count)| {
51                let priority = match layer {
52                    ArchitecturalLayer::Business => 100,
53                    ArchitecturalLayer::Data => 90,
54                    ArchitecturalLayer::Presentation => 80,
55                    ArchitecturalLayer::Infrastructure => 70,
56                    ArchitecturalLayer::Cross => 60,
57                };
58                priority + count
59            })
60            .map(|(layer, _)| layer)
61            .unwrap_or(ArchitecturalLayer::Cross)
62    }
63
64    /// Determine if the file changes suggest a significant architectural change
65    pub fn is_architectural_change(contexts: &[FileContext]) -> bool {
66        let critical_files = contexts
67            .iter()
68            .filter(|c| matches!(c.project_significance, ProjectSignificance::Critical))
69            .count();
70
71        let breaking_changes = contexts
72            .iter()
73            .filter(|c| {
74                matches!(
75                    c.change_impact,
76                    ChangeImpact::Breaking | ChangeImpact::Critical
77                )
78            })
79            .count();
80
81        critical_files > 0 || breaking_changes > 1 || contexts.len() > 10
82    }
83}
84
85/// Determine the purpose of a file based on its path and name
86fn determine_file_purpose(path: &Path) -> FilePurpose {
87    let path_str = path.to_string_lossy().to_lowercase();
88    let file_name = path
89        .file_name()
90        .and_then(|name| name.to_str())
91        .unwrap_or("")
92        .to_lowercase();
93
94    // Configuration files
95    if is_config_file(&path_str, &file_name) {
96        return FilePurpose::Config;
97    }
98
99    // Test files
100    if is_test_file(&path_str, &file_name) {
101        return FilePurpose::Test;
102    }
103
104    // Documentation files
105    if is_documentation_file(&path_str, &file_name) {
106        return FilePurpose::Documentation;
107    }
108
109    // Build and tooling files
110    if is_build_file(&path_str, &file_name) {
111        return FilePurpose::Build;
112    }
113
114    // Development tooling
115    if is_tooling_file(&path_str, &file_name) {
116        return FilePurpose::Tooling;
117    }
118
119    // Interface/API files
120    if is_interface_file(&path_str, &file_name) {
121        return FilePurpose::Interface;
122    }
123
124    // Default to core logic
125    FilePurpose::CoreLogic
126}
127
128/// Determine the architectural layer of a file
129fn determine_architectural_layer(path: &Path, file_purpose: &FilePurpose) -> ArchitecturalLayer {
130    let path_str = path.to_string_lossy().to_lowercase();
131
132    match file_purpose {
133        FilePurpose::Config | FilePurpose::Build | FilePurpose::Tooling => {
134            ArchitecturalLayer::Infrastructure
135        }
136        FilePurpose::Test | FilePurpose::Documentation => ArchitecturalLayer::Cross,
137        FilePurpose::Interface => ArchitecturalLayer::Presentation,
138        FilePurpose::CoreLogic => {
139            // Analyze path to determine specific layer
140            if path_str.contains("ui") || path_str.contains("web") || path_str.contains("cli") {
141                ArchitecturalLayer::Presentation
142            } else if path_str.contains("data")
143                || path_str.contains("db")
144                || path_str.contains("storage")
145            {
146                ArchitecturalLayer::Data
147            } else if path_str.contains("core")
148                || path_str.contains("business")
149                || path_str.contains("logic")
150            {
151                ArchitecturalLayer::Business
152            } else if path_str.contains("infra")
153                || path_str.contains("system")
154                || path_str.contains("network")
155            {
156                ArchitecturalLayer::Infrastructure
157            } else {
158                ArchitecturalLayer::Business // Default assumption
159            }
160        }
161    }
162}
163
164/// Determine the impact of changes based on change type and file purpose
165fn determine_change_impact(change_type: &str, file_purpose: &FilePurpose) -> ChangeImpact {
166    match change_type {
167        "A" => ChangeImpact::Additive, // Added file
168        "D" => {
169            // Deleted file - could be breaking depending on purpose
170            match file_purpose {
171                FilePurpose::Interface | FilePurpose::CoreLogic => ChangeImpact::Breaking,
172                _ => ChangeImpact::Modification,
173            }
174        }
175        "M" => {
176            // Modified file - depends on purpose
177            match file_purpose {
178                FilePurpose::Config => ChangeImpact::Modification,
179                FilePurpose::Test | FilePurpose::Documentation => ChangeImpact::Style,
180                FilePurpose::Interface => ChangeImpact::Breaking, // Potentially breaking
181                _ => ChangeImpact::Modification,
182            }
183        }
184        "R" => ChangeImpact::Modification, // Renamed
185        "C" => ChangeImpact::Additive,     // Copied
186        _ => ChangeImpact::Modification,   // Unknown change type
187    }
188}
189
190/// Determine the significance of a file in the project
191fn determine_project_significance(path: &Path, file_purpose: &FilePurpose) -> ProjectSignificance {
192    let path_str = path.to_string_lossy().to_lowercase();
193    let file_name = path
194        .file_name()
195        .and_then(|name| name.to_str())
196        .unwrap_or("")
197        .to_lowercase();
198
199    // Critical files
200    if is_critical_file(&path_str, &file_name) {
201        return ProjectSignificance::Critical;
202    }
203
204    // Important files based on purpose
205    match file_purpose {
206        FilePurpose::Interface | FilePurpose::CoreLogic => ProjectSignificance::Important,
207        FilePurpose::Config => {
208            if file_name.contains("cargo.toml") || file_name.contains("package.json") {
209                ProjectSignificance::Critical
210            } else {
211                ProjectSignificance::Important
212            }
213        }
214        FilePurpose::Test | FilePurpose::Documentation | FilePurpose::Tooling => {
215            ProjectSignificance::Routine
216        }
217        FilePurpose::Build => ProjectSignificance::Important,
218    }
219}
220
221/// Check if file is a configuration file
222fn is_config_file(path_str: &str, file_name: &str) -> bool {
223    let config_patterns = [
224        ".toml",
225        ".json",
226        ".yaml",
227        ".yml",
228        ".ini",
229        ".cfg",
230        ".conf",
231        ".env",
232        ".properties",
233        "config",
234        "settings",
235        "options",
236    ];
237
238    let config_names = [
239        "cargo.toml",
240        "package.json",
241        "pyproject.toml",
242        "go.mod",
243        "pom.xml",
244        "build.gradle",
245        "makefile",
246        "dockerfile",
247        ".gitignore",
248        ".gitattributes",
249    ];
250
251    config_patterns
252        .iter()
253        .any(|pattern| file_name.contains(pattern))
254        || config_names.contains(&file_name)
255        || path_str.contains("config")
256        || path_str.contains(".github/workflows")
257}
258
259/// Check if file is a test file
260fn is_test_file(path_str: &str, file_name: &str) -> bool {
261    path_str.contains("test")
262        || path_str.contains("spec")
263        || file_name.contains("test")
264        || file_name.contains("spec")
265        || file_name.ends_with("_test.rs")
266        || file_name.ends_with("_test.py")
267        || file_name.ends_with(".test.js")
268        || file_name.ends_with("_test.go")
269}
270
271/// Check if file is documentation
272fn is_documentation_file(path_str: &str, file_name: &str) -> bool {
273    let doc_extensions = [".md", ".rst", ".txt", ".adoc"];
274    let doc_names = ["readme", "changelog", "contributing", "license", "authors"];
275
276    doc_extensions.iter().any(|ext| file_name.ends_with(ext))
277        || doc_names.iter().any(|name| file_name.contains(name))
278        || path_str.contains("doc")
279        || path_str.contains("guide")
280        || path_str.contains("manual")
281}
282
283/// Check if file is build-related
284fn is_build_file(path_str: &str, file_name: &str) -> bool {
285    let build_names = [
286        "makefile",
287        "dockerfile",
288        "build.gradle",
289        "pom.xml",
290        "cmake",
291        "webpack.config",
292        "rollup.config",
293        "vite.config",
294    ];
295
296    build_names.iter().any(|name| file_name.contains(name))
297        || path_str.contains("build")
298        || path_str.contains("scripts")
299        || file_name.ends_with(".sh")
300        || file_name.ends_with(".bat")
301}
302
303/// Check if file is tooling/development related
304fn is_tooling_file(path_str: &str, file_name: &str) -> bool {
305    path_str.contains("tool")
306        || path_str.contains("util")
307        || path_str.contains(".vscode")
308        || path_str.contains(".idea")
309        || file_name.starts_with(".")
310        || file_name.contains("prettier")
311        || file_name.contains("eslint")
312        || file_name.contains("clippy")
313}
314
315/// Check if file defines interfaces/APIs
316fn is_interface_file(path_str: &str, file_name: &str) -> bool {
317    path_str.contains("api")
318        || path_str.contains("interface")
319        || path_str.contains("proto")
320        || file_name.contains("lib.rs")
321        || file_name.contains("mod.rs")
322        || file_name.contains("index")
323        || file_name.ends_with(".proto")
324        || file_name.ends_with(".graphql")
325}
326
327/// Check if file is critical to project functionality
328fn is_critical_file(path_str: &str, file_name: &str) -> bool {
329    let critical_names = [
330        "main.rs",
331        "lib.rs",
332        "index.js",
333        "app.js",
334        "main.py",
335        "__init__.py",
336        "main.go",
337        "main.java",
338        "cargo.toml",
339        "package.json",
340        "go.mod",
341        "pom.xml",
342    ];
343
344    critical_names.contains(&file_name)
345        || (path_str.contains("src") && (file_name == "lib.rs" || file_name == "main.rs"))
346}