Skip to main content

omni_dev/claude/context/
files.rs

1//! File-based context analysis and architectural understanding.
2
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use crate::data::context::{
7    ArchitecturalLayer, ChangeImpact, FileContext, FilePurpose, ProjectSignificance,
8};
9use crate::git::CommitInfo;
10
11/// File context analyzer.
12pub struct FileAnalyzer;
13
14impl FileAnalyzer {
15    /// Analyzes a file and determines its context within the project.
16    pub fn analyze_file(path: &Path, change_type: &str) -> FileContext {
17        let file_purpose = determine_file_purpose(path);
18        let architectural_layer = determine_architectural_layer(path, &file_purpose);
19        let change_impact = determine_change_impact(change_type, &file_purpose);
20        let project_significance = determine_project_significance(path, &file_purpose);
21
22        FileContext {
23            path: path.to_path_buf(),
24            file_purpose,
25            architectural_layer,
26            change_impact,
27            project_significance,
28        }
29    }
30
31    /// Analyzes multiple files to understand the scope of changes.
32    pub fn analyze_file_set(files: &[(PathBuf, String)]) -> Vec<FileContext> {
33        files
34            .iter()
35            .map(|(path, change_type)| Self::analyze_file(path, change_type))
36            .collect()
37    }
38
39    /// Analyzes file changes across a range of commits, deduplicating by path.
40    ///
41    /// When a file appears in multiple commits, the status from the last
42    /// (most recent) commit wins. This provides the most accurate signal
43    /// for significance analysis.
44    pub fn analyze_commits(commits: &[CommitInfo]) -> Vec<FileContext> {
45        let mut file_map: HashMap<PathBuf, String> = HashMap::new();
46
47        for commit in commits {
48            for fc in &commit.analysis.file_changes.file_list {
49                file_map.insert(PathBuf::from(&fc.file), fc.status.clone());
50            }
51        }
52
53        let files: Vec<(PathBuf, String)> = file_map.into_iter().collect();
54        Self::analyze_file_set(&files)
55    }
56
57    /// Determines the primary architectural impact of a set of file changes.
58    pub fn primary_architectural_impact(contexts: &[FileContext]) -> ArchitecturalLayer {
59        let mut layer_counts = HashMap::new();
60        for context in contexts {
61            *layer_counts
62                .entry(context.architectural_layer.clone())
63                .or_insert(0) += 1;
64        }
65
66        // Return the most common architectural layer, with precedence for critical layers
67        layer_counts
68            .into_iter()
69            .max_by_key(|(layer, count)| {
70                let priority = match layer {
71                    ArchitecturalLayer::Business => 100,
72                    ArchitecturalLayer::Data => 90,
73                    ArchitecturalLayer::Presentation => 80,
74                    ArchitecturalLayer::Infrastructure => 70,
75                    ArchitecturalLayer::Cross => 60,
76                };
77                priority + count
78            })
79            .map_or(ArchitecturalLayer::Cross, |(layer, _)| layer)
80    }
81
82    /// Determines if the file changes suggest a significant architectural change.
83    #[must_use]
84    pub fn is_architectural_change(contexts: &[FileContext]) -> bool {
85        let critical_files = contexts
86            .iter()
87            .filter(|c| matches!(c.project_significance, ProjectSignificance::Critical))
88            .count();
89
90        let breaking_changes = contexts
91            .iter()
92            .filter(|c| {
93                matches!(
94                    c.change_impact,
95                    ChangeImpact::Breaking | ChangeImpact::Critical
96                )
97            })
98            .count();
99
100        critical_files > 0 || breaking_changes > 1 || contexts.len() > 10
101    }
102}
103
104/// Determines the purpose of a file based on its path and name.
105fn determine_file_purpose(path: &Path) -> FilePurpose {
106    let path_str = path.to_string_lossy().to_lowercase();
107    let file_name = path
108        .file_name()
109        .and_then(|name| name.to_str())
110        .unwrap_or("")
111        .to_lowercase();
112
113    // Configuration files
114    if is_config_file(&path_str, &file_name) {
115        return FilePurpose::Config;
116    }
117
118    // Test files
119    if is_test_file(&path_str, &file_name) {
120        return FilePurpose::Test;
121    }
122
123    // Documentation files
124    if is_documentation_file(&path_str, &file_name) {
125        return FilePurpose::Documentation;
126    }
127
128    // Build and tooling files
129    if is_build_file(&path_str, &file_name) {
130        return FilePurpose::Build;
131    }
132
133    // Development tooling
134    if is_tooling_file(&path_str, &file_name) {
135        return FilePurpose::Tooling;
136    }
137
138    // Interface/API files
139    if is_interface_file(&path_str, &file_name) {
140        return FilePurpose::Interface;
141    }
142
143    // Default to core logic
144    FilePurpose::CoreLogic
145}
146
147/// Determines the architectural layer of a file.
148fn determine_architectural_layer(path: &Path, file_purpose: &FilePurpose) -> ArchitecturalLayer {
149    let path_str = path.to_string_lossy().to_lowercase();
150
151    match file_purpose {
152        FilePurpose::Config | FilePurpose::Build | FilePurpose::Tooling => {
153            ArchitecturalLayer::Infrastructure
154        }
155        FilePurpose::Test | FilePurpose::Documentation => ArchitecturalLayer::Cross,
156        FilePurpose::Interface => ArchitecturalLayer::Presentation,
157        FilePurpose::CoreLogic => {
158            // Analyze path to determine specific layer
159            if path_str.contains("ui") || path_str.contains("web") || path_str.contains("cli") {
160                ArchitecturalLayer::Presentation
161            } else if path_str.contains("data")
162                || path_str.contains("db")
163                || path_str.contains("storage")
164            {
165                ArchitecturalLayer::Data
166            } else if path_str.contains("core")
167                || path_str.contains("business")
168                || path_str.contains("logic")
169            {
170                ArchitecturalLayer::Business
171            } else if path_str.contains("infra")
172                || path_str.contains("system")
173                || path_str.contains("network")
174            {
175                ArchitecturalLayer::Infrastructure
176            } else {
177                ArchitecturalLayer::Business // Default assumption
178            }
179        }
180    }
181}
182
183/// Determines the impact of changes based on change type and file purpose.
184fn determine_change_impact(change_type: &str, file_purpose: &FilePurpose) -> ChangeImpact {
185    match change_type {
186        "A" | "C" => ChangeImpact::Additive, // Added or Copied
187        "D" => {
188            // Deleted file - could be breaking depending on purpose
189            match file_purpose {
190                FilePurpose::Interface | FilePurpose::CoreLogic => ChangeImpact::Breaking,
191                _ => ChangeImpact::Modification,
192            }
193        }
194        "M" => {
195            // Modified file - depends on purpose
196            match file_purpose {
197                FilePurpose::Test | FilePurpose::Documentation => ChangeImpact::Style,
198                FilePurpose::Interface => ChangeImpact::Breaking, // Potentially breaking
199                _ => ChangeImpact::Modification,
200            }
201        }
202        _ => ChangeImpact::Modification, // Renamed, unknown, etc.
203    }
204}
205
206/// Determines the significance of a file in the project.
207fn determine_project_significance(path: &Path, file_purpose: &FilePurpose) -> ProjectSignificance {
208    let path_str = path.to_string_lossy().to_lowercase();
209    let file_name = path
210        .file_name()
211        .and_then(|name| name.to_str())
212        .unwrap_or("")
213        .to_lowercase();
214
215    // Critical files
216    if is_critical_file(&path_str, &file_name) {
217        return ProjectSignificance::Critical;
218    }
219
220    // Important files based on purpose
221    match file_purpose {
222        FilePurpose::Interface | FilePurpose::CoreLogic | FilePurpose::Build => {
223            ProjectSignificance::Important
224        }
225        FilePurpose::Config => {
226            if file_name.contains("cargo.toml") || file_name.contains("package.json") {
227                ProjectSignificance::Critical
228            } else {
229                ProjectSignificance::Important
230            }
231        }
232        FilePurpose::Test | FilePurpose::Documentation | FilePurpose::Tooling => {
233            ProjectSignificance::Routine
234        }
235    }
236}
237
238/// Checks if a file is a configuration file.
239fn is_config_file(path_str: &str, file_name: &str) -> bool {
240    let config_patterns = [
241        ".toml",
242        ".json",
243        ".yaml",
244        ".yml",
245        ".ini",
246        ".cfg",
247        ".conf",
248        ".env",
249        ".properties",
250        "config",
251        "settings",
252        "options",
253    ];
254
255    let config_names = [
256        "cargo.toml",
257        "package.json",
258        "pyproject.toml",
259        "go.mod",
260        "pom.xml",
261        "build.gradle",
262        "makefile",
263        "dockerfile",
264        ".gitignore",
265        ".gitattributes",
266    ];
267
268    config_patterns
269        .iter()
270        .any(|pattern| file_name.contains(pattern))
271        || config_names.contains(&file_name)
272        || path_str.contains("config")
273        || path_str.contains(".github/workflows")
274}
275
276/// Checks if a file is a test file.
277fn is_test_file(path_str: &str, file_name: &str) -> bool {
278    path_str.contains("test")
279        || path_str.contains("spec")
280        || file_name.contains("test")
281        || file_name.contains("spec")
282        || file_name.ends_with("_test.rs")
283        || file_name.ends_with("_test.py")
284        || file_name.ends_with(".test.js")
285        || file_name.ends_with("_test.go")
286}
287
288/// Checks if a file is documentation.
289fn is_documentation_file(path_str: &str, file_name: &str) -> bool {
290    let doc_extensions = [".md", ".rst", ".txt", ".adoc"];
291    let doc_names = ["readme", "changelog", "contributing", "license", "authors"];
292
293    doc_extensions.iter().any(|ext| file_name.ends_with(ext))
294        || doc_names.iter().any(|name| file_name.contains(name))
295        || path_str.contains("doc")
296        || path_str.contains("guide")
297        || path_str.contains("manual")
298}
299
300/// Checks if a file is build-related.
301fn is_build_file(path_str: &str, file_name: &str) -> bool {
302    let build_names = [
303        "makefile",
304        "dockerfile",
305        "build.gradle",
306        "pom.xml",
307        "cmake",
308        "webpack.config",
309        "rollup.config",
310        "vite.config",
311    ];
312
313    build_names.iter().any(|name| file_name.contains(name))
314        || path_str.contains("build")
315        || path_str.contains("scripts")
316        || file_name.ends_with(".sh")
317        || file_name.ends_with(".bat")
318}
319
320/// Checks if a file is tooling/development related.
321fn is_tooling_file(path_str: &str, file_name: &str) -> bool {
322    path_str.contains("tool")
323        || path_str.contains("util")
324        || path_str.contains(".vscode")
325        || path_str.contains(".idea")
326        || file_name.starts_with('.')
327        || file_name.contains("prettier")
328        || file_name.contains("eslint")
329        || file_name.contains("clippy")
330}
331
332/// Checks if a file defines interfaces/APIs.
333fn is_interface_file(path_str: &str, file_name: &str) -> bool {
334    path_str.contains("api")
335        || path_str.contains("interface")
336        || path_str.contains("proto")
337        || file_name.contains("lib.rs")
338        || file_name.contains("mod.rs")
339        || file_name.contains("index")
340        || file_name.ends_with(".proto")
341        || file_name.ends_with(".graphql")
342}
343
344/// Checks if a file is critical to project functionality.
345fn is_critical_file(path_str: &str, file_name: &str) -> bool {
346    let critical_names = [
347        "main.rs",
348        "lib.rs",
349        "index.js",
350        "app.js",
351        "main.py",
352        "__init__.py",
353        "main.go",
354        "main.java",
355        "cargo.toml",
356        "package.json",
357        "go.mod",
358        "pom.xml",
359    ];
360
361    critical_names.contains(&file_name)
362        || (path_str.contains("src") && (file_name == "lib.rs" || file_name == "main.rs"))
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use std::path::Path;
369
370    // ── determine_file_purpose ─────────────────────────────────────
371
372    #[test]
373    fn purpose_config_toml() {
374        assert!(matches!(
375            determine_file_purpose(Path::new("Cargo.toml")),
376            FilePurpose::Config
377        ));
378    }
379
380    #[test]
381    fn purpose_config_json() {
382        assert!(matches!(
383            determine_file_purpose(Path::new("package.json")),
384            FilePurpose::Config
385        ));
386    }
387
388    #[test]
389    fn purpose_test_file() {
390        assert!(matches!(
391            determine_file_purpose(Path::new("tests/integration_test.rs")),
392            FilePurpose::Test
393        ));
394    }
395
396    #[test]
397    fn purpose_documentation() {
398        assert!(matches!(
399            determine_file_purpose(Path::new("README.md")),
400            FilePurpose::Documentation
401        ));
402    }
403
404    #[test]
405    fn purpose_build_file() {
406        assert!(matches!(
407            determine_file_purpose(Path::new("scripts/build.sh")),
408            FilePurpose::Build
409        ));
410    }
411
412    #[test]
413    fn purpose_interface_file() {
414        assert!(matches!(
415            determine_file_purpose(Path::new("src/api/handler.rs")),
416            FilePurpose::Interface
417        ));
418    }
419
420    #[test]
421    fn purpose_core_logic_default() {
422        assert!(matches!(
423            determine_file_purpose(Path::new("src/claude/prompts.rs")),
424            FilePurpose::CoreLogic
425        ));
426    }
427
428    // ── determine_architectural_layer ──────────────────────────────
429
430    #[test]
431    fn layer_config_is_infrastructure() {
432        let layer = determine_architectural_layer(Path::new("Cargo.toml"), &FilePurpose::Config);
433        assert_eq!(layer, ArchitecturalLayer::Infrastructure);
434    }
435
436    #[test]
437    fn layer_test_is_cross() {
438        let layer = determine_architectural_layer(Path::new("tests/test.rs"), &FilePurpose::Test);
439        assert_eq!(layer, ArchitecturalLayer::Cross);
440    }
441
442    #[test]
443    fn layer_interface_is_presentation() {
444        let layer =
445            determine_architectural_layer(Path::new("src/api/mod.rs"), &FilePurpose::Interface);
446        assert_eq!(layer, ArchitecturalLayer::Presentation);
447    }
448
449    #[test]
450    fn layer_cli_is_presentation() {
451        let layer =
452            determine_architectural_layer(Path::new("src/cli/git.rs"), &FilePurpose::CoreLogic);
453        assert_eq!(layer, ArchitecturalLayer::Presentation);
454    }
455
456    #[test]
457    fn layer_data_is_data() {
458        let layer =
459            determine_architectural_layer(Path::new("src/data/check.rs"), &FilePurpose::CoreLogic);
460        assert_eq!(layer, ArchitecturalLayer::Data);
461    }
462
463    #[test]
464    fn layer_core_is_business() {
465        let layer =
466            determine_architectural_layer(Path::new("src/core/engine.rs"), &FilePurpose::CoreLogic);
467        assert_eq!(layer, ArchitecturalLayer::Business);
468    }
469
470    #[test]
471    fn layer_unknown_defaults_business() {
472        let layer = determine_architectural_layer(
473            Path::new("src/claude/prompts.rs"),
474            &FilePurpose::CoreLogic,
475        );
476        assert_eq!(layer, ArchitecturalLayer::Business);
477    }
478
479    // ── determine_change_impact ────────────────────────────────────
480
481    #[test]
482    fn impact_added_is_additive() {
483        assert!(matches!(
484            determine_change_impact("A", &FilePurpose::CoreLogic),
485            ChangeImpact::Additive
486        ));
487    }
488
489    #[test]
490    fn impact_deleted_interface_is_breaking() {
491        assert!(matches!(
492            determine_change_impact("D", &FilePurpose::Interface),
493            ChangeImpact::Breaking
494        ));
495    }
496
497    #[test]
498    fn impact_deleted_test_is_modification() {
499        assert!(matches!(
500            determine_change_impact("D", &FilePurpose::Test),
501            ChangeImpact::Modification
502        ));
503    }
504
505    #[test]
506    fn impact_modified_test_is_style() {
507        assert!(matches!(
508            determine_change_impact("M", &FilePurpose::Test),
509            ChangeImpact::Style
510        ));
511    }
512
513    #[test]
514    fn impact_modified_core_is_modification() {
515        assert!(matches!(
516            determine_change_impact("M", &FilePurpose::CoreLogic),
517            ChangeImpact::Modification
518        ));
519    }
520
521    #[test]
522    fn impact_unknown_type_is_modification() {
523        assert!(matches!(
524            determine_change_impact("R", &FilePurpose::CoreLogic),
525            ChangeImpact::Modification
526        ));
527    }
528
529    // ── determine_project_significance ─────────────────────────────
530
531    #[test]
532    fn significance_main_rs_is_critical() {
533        assert!(matches!(
534            determine_project_significance(Path::new("src/main.rs"), &FilePurpose::CoreLogic),
535            ProjectSignificance::Critical
536        ));
537    }
538
539    #[test]
540    fn significance_cargo_toml_is_critical() {
541        assert!(matches!(
542            determine_project_significance(Path::new("Cargo.toml"), &FilePurpose::Config),
543            ProjectSignificance::Critical
544        ));
545    }
546
547    #[test]
548    fn significance_core_logic_is_important() {
549        assert!(matches!(
550            determine_project_significance(
551                Path::new("src/claude/prompts.rs"),
552                &FilePurpose::CoreLogic
553            ),
554            ProjectSignificance::Important
555        ));
556    }
557
558    #[test]
559    fn significance_test_is_routine() {
560        assert!(matches!(
561            determine_project_significance(Path::new("tests/test.rs"), &FilePurpose::Test),
562            ProjectSignificance::Routine
563        ));
564    }
565
566    // ── is_* helper functions ──────────────────────────────────────
567
568    #[test]
569    fn test_file_detected() {
570        assert!(is_test_file("tests/integration.rs", "integration.rs"));
571        assert!(is_test_file("src/foo_test.rs", "foo_test.rs"));
572        assert!(!is_test_file("src/main.rs", "main.rs"));
573    }
574
575    #[test]
576    fn documentation_file_detected() {
577        assert!(is_documentation_file("README.md", "readme.md"));
578        assert!(is_documentation_file("docs/guide.md", "guide.md"));
579        assert!(!is_documentation_file("src/main.rs", "main.rs"));
580    }
581
582    #[test]
583    fn build_file_detected() {
584        assert!(is_build_file("scripts/deploy.sh", "deploy.sh"));
585        assert!(is_build_file("Makefile", "makefile"));
586        assert!(!is_build_file("src/main.rs", "main.rs"));
587    }
588
589    #[test]
590    fn interface_file_detected() {
591        assert!(is_interface_file("src/api/routes.rs", "routes.rs"));
592        assert!(is_interface_file("protos/service.proto", "service.proto"));
593        assert!(!is_interface_file("src/claude/prompts.rs", "prompts.rs"));
594    }
595
596    // ── FileAnalyzer ───────────────────────────────────────────────
597
598    #[test]
599    fn analyze_file_rust_source() {
600        let ctx = FileAnalyzer::analyze_file(Path::new("src/claude/prompts.rs"), "M");
601        assert!(matches!(ctx.file_purpose, FilePurpose::CoreLogic));
602        assert!(matches!(ctx.change_impact, ChangeImpact::Modification));
603        assert!(matches!(
604            ctx.project_significance,
605            ProjectSignificance::Important
606        ));
607    }
608
609    #[test]
610    fn analyze_file_set_multiple() {
611        let files = vec![
612            (PathBuf::from("src/main.rs"), "M".to_string()),
613            (PathBuf::from("README.md"), "M".to_string()),
614        ];
615        let contexts = FileAnalyzer::analyze_file_set(&files);
616        assert_eq!(contexts.len(), 2);
617    }
618
619    #[test]
620    fn primary_architectural_impact_mixed() {
621        let contexts = vec![
622            FileAnalyzer::analyze_file(Path::new("src/data/check.rs"), "M"),
623            FileAnalyzer::analyze_file(Path::new("src/data/yaml.rs"), "M"),
624            FileAnalyzer::analyze_file(Path::new("README.md"), "M"),
625        ];
626        let layer = FileAnalyzer::primary_architectural_impact(&contexts);
627        assert_eq!(layer, ArchitecturalLayer::Data);
628    }
629
630    #[test]
631    fn primary_architectural_impact_empty() {
632        let layer = FileAnalyzer::primary_architectural_impact(&[]);
633        assert_eq!(layer, ArchitecturalLayer::Cross);
634    }
635
636    #[test]
637    fn is_architectural_change_critical_files() {
638        let contexts = vec![FileAnalyzer::analyze_file(Path::new("src/main.rs"), "D")];
639        assert!(FileAnalyzer::is_architectural_change(&contexts));
640    }
641
642    #[test]
643    fn is_architectural_change_many_files() {
644        let contexts: Vec<_> = (0..11)
645            .map(|i| FileAnalyzer::analyze_file(Path::new(&format!("src/file{i}.rs")), "M"))
646            .collect();
647        assert!(FileAnalyzer::is_architectural_change(&contexts));
648    }
649
650    #[test]
651    fn is_not_architectural_change_small() {
652        let contexts = vec![FileAnalyzer::analyze_file(
653            Path::new("src/claude/prompts.rs"),
654            "M",
655        )];
656        assert!(!FileAnalyzer::is_architectural_change(&contexts));
657    }
658
659    // ── FileAnalyzer::analyze_commits ─────────────────────────────
660
661    mod analyze_commits_tests {
662        use super::*;
663        use crate::git::commit::{CommitAnalysis, FileChange, FileChanges};
664
665        fn make_commit(files: Vec<(&str, &str)>) -> CommitInfo {
666            CommitInfo {
667                hash: "a".repeat(40),
668                author: "Test <test@test.com>".to_string(),
669                date: chrono::Utc::now().fixed_offset(),
670                original_message: "test commit".to_string(),
671                in_main_branches: Vec::new(),
672                analysis: CommitAnalysis {
673                    detected_type: String::new(),
674                    detected_scope: String::new(),
675                    proposed_message: String::new(),
676                    file_changes: FileChanges {
677                        total_files: files.len(),
678                        files_added: files.iter().filter(|(s, _)| *s == "A").count(),
679                        files_deleted: files.iter().filter(|(s, _)| *s == "D").count(),
680                        file_list: files
681                            .into_iter()
682                            .map(|(status, file)| FileChange {
683                                status: status.to_string(),
684                                file: file.to_string(),
685                            })
686                            .collect(),
687                    },
688                    diff_summary: String::new(),
689                    diff_file: String::new(),
690                    file_diffs: Vec::new(),
691                },
692            }
693        }
694
695        #[test]
696        fn empty_commits() {
697            let result = FileAnalyzer::analyze_commits(&[]);
698            assert!(result.is_empty());
699        }
700
701        #[test]
702        fn single_commit() {
703            let commit = make_commit(vec![("M", "src/main.rs"), ("A", "src/new.rs")]);
704            let result = FileAnalyzer::analyze_commits(&[commit]);
705            assert_eq!(result.len(), 2);
706        }
707
708        #[test]
709        fn deduplicates_across_commits() {
710            let commits = vec![
711                make_commit(vec![("A", "src/feature.rs"), ("M", "src/lib.rs")]),
712                make_commit(vec![("M", "src/feature.rs"), ("M", "src/main.rs")]),
713            ];
714            let result = FileAnalyzer::analyze_commits(&commits);
715            // 3 unique files: src/feature.rs, src/lib.rs, src/main.rs
716            assert_eq!(result.len(), 3);
717        }
718
719        #[test]
720        fn last_status_wins() {
721            let commits = vec![
722                make_commit(vec![("A", "src/feature.rs")]),
723                make_commit(vec![("M", "src/feature.rs")]),
724            ];
725            let result = FileAnalyzer::analyze_commits(&commits);
726            assert_eq!(result.len(), 1);
727            // Added then Modified — last status (M) wins → Modification impact
728            assert!(matches!(
729                result[0].change_impact,
730                ChangeImpact::Modification
731            ));
732        }
733    }
734
735    // ── property tests ────────────────────────────────────────────
736
737    mod prop {
738        use super::*;
739        use proptest::prelude::*;
740
741        fn arb_file_purpose() -> impl Strategy<Value = FilePurpose> {
742            prop_oneof![
743                Just(FilePurpose::Config),
744                Just(FilePurpose::Test),
745                Just(FilePurpose::Documentation),
746                Just(FilePurpose::Build),
747                Just(FilePurpose::Tooling),
748                Just(FilePurpose::Interface),
749                Just(FilePurpose::CoreLogic),
750            ]
751        }
752
753        proptest! {
754            #[test]
755            fn file_purpose_deterministic(s in "[a-zA-Z0-9_/\\.]{0,100}") {
756                let p = Path::new(&s);
757                let a = format!("{:?}", determine_file_purpose(p));
758                let b = format!("{:?}", determine_file_purpose(p));
759                prop_assert_eq!(a, b);
760            }
761
762            #[test]
763            fn config_extensions_classified(
764                name in "[a-z]{1,10}",
765                ext in prop_oneof![
766                    Just(".toml"),
767                    Just(".json"),
768                    Just(".yaml"),
769                    Just(".yml"),
770                    Just(".ini"),
771                    Just(".cfg"),
772                ],
773            ) {
774                let path_str = format!("{name}{ext}");
775                let purpose = determine_file_purpose(Path::new(&path_str));
776                prop_assert!(matches!(purpose, FilePurpose::Config));
777            }
778
779            #[test]
780            fn test_paths_classified(name in "[a-z_]{1,20}\\.rs") {
781                let path_str = format!("tests/{name}");
782                let purpose = determine_file_purpose(Path::new(&path_str));
783                prop_assert!(matches!(purpose, FilePurpose::Test));
784            }
785
786            #[test]
787            fn change_impact_added_always_additive(purpose in arb_file_purpose()) {
788                let impact = determine_change_impact("A", &purpose);
789                prop_assert!(matches!(impact, ChangeImpact::Additive));
790            }
791
792            #[test]
793            fn architectural_layer_deterministic(
794                s in "[a-zA-Z0-9_/\\.]{0,100}",
795                purpose in arb_file_purpose(),
796            ) {
797                let p = Path::new(&s);
798                let a = format!("{:?}", determine_architectural_layer(p, &purpose));
799                let b = format!("{:?}", determine_architectural_layer(p, &purpose));
800                prop_assert_eq!(a, b);
801            }
802        }
803    }
804}