Skip to main content

flat/
priority.rs

1use std::path::Path;
2
3/// Score a file for priority ordering in token budget allocation.
4///
5/// Higher scores = higher priority (included first in budget).
6/// Per PDR spec:
7/// - READMEs: 100
8/// - Entry points (main.*, index.*, app.*): 90
9/// - Config files: 80
10/// - Source code: 70 - (depth * 10), min 10
11/// - Tests: 30
12/// - Fixtures/generated: 5
13pub fn score_file(path: &Path, base_path: &Path) -> u32 {
14    let file_name = path
15        .file_name()
16        .map(|f| f.to_string_lossy().to_lowercase())
17        .unwrap_or_default();
18
19    let relative = path.strip_prefix(base_path).unwrap_or(path);
20    let depth = relative.components().count().saturating_sub(1); // depth of file, not dir
21
22    let path_str = relative.to_string_lossy().to_lowercase();
23
24    // Check categories in priority order (highest score wins)
25    if is_fixture(&path_str) {
26        5
27    } else if is_test(&path_str, &file_name) {
28        30
29    } else if is_readme(&file_name) {
30        100
31    } else if is_entry_point(&file_name) {
32        90
33    } else if is_config(&file_name) {
34        80
35    } else {
36        // Source code with depth penalty
37        let score = 70u32.saturating_sub((depth as u32) * 10);
38        score.max(10)
39    }
40}
41
42fn is_readme(file_name: &str) -> bool {
43    file_name.starts_with("readme")
44}
45
46fn is_entry_point(file_name: &str) -> bool {
47    let stem = file_name.split('.').next().unwrap_or("");
48    matches!(stem, "main" | "index" | "app" | "lib" | "mod")
49}
50
51fn is_config(file_name: &str) -> bool {
52    let stem = file_name.split('.').next().unwrap_or("");
53    matches!(
54        stem,
55        "config"
56            | "settings"
57            | "package"
58            | "cargo"
59            | "tsconfig"
60            | "webpack"
61            | "vite"
62            | "eslint"
63            | "prettier"
64            | "jest"
65            | "pyproject"
66            | "setup"
67            | "makefile"
68            | "dockerfile"
69            | "docker-compose"
70            | "go"
71    ) || file_name.ends_with(".toml")
72        || file_name.ends_with(".yaml")
73        || file_name.ends_with(".yml")
74        || file_name.ends_with(".json") && !file_name.contains("test")
75}
76
77fn is_test(path_str: &str, file_name: &str) -> bool {
78    path_str.contains("test")
79        || path_str.contains("spec")
80        || file_name.contains("test")
81        || file_name.contains("spec")
82}
83
84fn is_fixture(path_str: &str) -> bool {
85    path_str.contains("fixture")
86        || path_str.contains("testdata")
87        || path_str.contains("test_data")
88        || path_str.contains("__snapshots__")
89        || path_str.contains("generated")
90        || path_str.contains("vendor")
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use std::path::PathBuf;
97
98    fn score(path: &str) -> u32 {
99        score_file(Path::new(path), Path::new("/project"))
100    }
101
102    #[test]
103    fn test_readme_highest() {
104        assert_eq!(score("/project/README.md"), 100);
105        assert_eq!(score("/project/readme.txt"), 100);
106    }
107
108    #[test]
109    fn test_entry_points() {
110        assert_eq!(score("/project/src/main.rs"), 90);
111        assert_eq!(score("/project/src/index.ts"), 90);
112        assert_eq!(score("/project/src/lib.rs"), 90);
113    }
114
115    #[test]
116    fn test_config_files() {
117        assert_eq!(score("/project/Cargo.toml"), 80);
118        assert_eq!(score("/project/package.json"), 80);
119    }
120
121    #[test]
122    fn test_source_with_depth_penalty() {
123        // depth 0 (file at root)
124        assert_eq!(score("/project/foo.rs"), 70);
125        // depth 1
126        assert_eq!(score("/project/src/foo.rs"), 60);
127        // depth 2
128        assert_eq!(score("/project/src/utils/foo.rs"), 50);
129        // depth 6+ → min 10
130        assert_eq!(score("/project/a/b/c/d/e/f/foo.rs"), 10);
131    }
132
133    #[test]
134    fn test_tests_scored_low() {
135        assert_eq!(score("/project/tests/unit_test.rs"), 30);
136        assert_eq!(score("/project/src/foo_test.go"), 30);
137    }
138
139    #[test]
140    fn test_fixtures_lowest() {
141        assert_eq!(score("/project/tests/fixtures/data.json"), 5);
142        assert_eq!(score("/project/testdata/input.txt"), 5);
143    }
144
145    #[test]
146    fn test_readme_in_subdirectory() {
147        // README in tests/fixtures/ → fixture (5), not README (100)
148        assert_eq!(score("/project/tests/fixtures/README.md"), 5);
149    }
150
151    #[test]
152    fn test_sorting_order() {
153        let base = PathBuf::from("/project");
154        let mut files = [
155            PathBuf::from("/project/tests/fixture/data.json"),
156            PathBuf::from("/project/src/utils.rs"),
157            PathBuf::from("/project/README.md"),
158            PathBuf::from("/project/src/main.rs"),
159            PathBuf::from("/project/Cargo.toml"),
160            PathBuf::from("/project/tests/test_foo.rs"),
161        ];
162
163        files.sort_by(|a, b| {
164            let sa = score_file(a, &base);
165            let sb = score_file(b, &base);
166            sb.cmp(&sa).then_with(|| a.cmp(b))
167        });
168
169        let names: Vec<&str> = files
170            .iter()
171            .map(|p| p.file_name().unwrap().to_str().unwrap())
172            .collect();
173        assert_eq!(names[0], "README.md");
174        assert_eq!(names[1], "main.rs");
175        assert_eq!(names[2], "Cargo.toml");
176    }
177}