Skip to main content

omni_dev/claude/context/
files.rs

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