1use crate::graph::FileSummary;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum FileRole {
10 Test,
11 Fixture,
12 Cli,
13 Parser,
14 QueryLayer,
15 Config,
16 Build,
17 Model,
18 View,
19 ApiServer,
20 Mcp,
21 Skill,
22 Entry,
23 Library,
24 Other,
25}
26
27impl FileRole {
28 pub fn label(self) -> &'static str {
29 match self {
30 FileRole::Test => "test",
31 FileRole::Fixture => "fixture",
32 FileRole::Cli => "cli",
33 FileRole::Parser => "parser",
34 FileRole::QueryLayer => "query",
35 FileRole::Config => "config",
36 FileRole::Build => "build",
37 FileRole::Model => "model",
38 FileRole::View => "view",
39 FileRole::ApiServer => "api",
40 FileRole::Mcp => "mcp",
41 FileRole::Skill => "skill",
42 FileRole::Entry => "entry",
43 FileRole::Library => "library",
44 FileRole::Other => "other",
45 }
46 }
47
48 pub fn description(self) -> &'static str {
50 match self {
51 FileRole::Test => "Test suite",
52 FileRole::Fixture => "Test fixture",
53 FileRole::Cli => "CLI command / argument plumbing",
54 FileRole::Parser => "Language parser (Tree-sitter front-end)",
55 FileRole::QueryLayer => "Graph query / data-access layer",
56 FileRole::Config => "Configuration & settings",
57 FileRole::Build => "Build / packaging glue",
58 FileRole::Model => "Domain model / data types",
59 FileRole::View => "UI component (renders to screen)",
60 FileRole::ApiServer => "HTTP API server / handler",
61 FileRole::Mcp => "MCP (Model Context Protocol) tool surface",
62 FileRole::Skill => "AI agent skill / instructions",
63 FileRole::Entry => "Program entry point",
64 FileRole::Library => "General-purpose library code",
65 FileRole::Other => "General module",
66 }
67 }
68}
69
70pub fn classify(summary: &FileSummary) -> FileRole {
72 let path = summary.path.to_lowercase();
73
74 if path.contains("/tests/")
76 || path.ends_with("_test.go")
77 || path.contains(".test.")
78 || path.ends_with("_test.rs")
79 || path.ends_with("_test.py")
80 || path.contains("/__tests__/")
81 {
82 return if path.contains("/fixtures/") || path.contains("/fixture/") {
83 FileRole::Fixture
84 } else {
85 FileRole::Test
86 };
87 }
88 if path.contains("/fixtures/") || path.contains("/fixture/") {
89 return FileRole::Fixture;
90 }
91
92 let basename = path.rsplit('/').next().unwrap_or(&path);
94 if matches!(
95 basename,
96 "main.rs"
97 | "main.go"
98 | "main.py"
99 | "main.ts"
100 | "main.js"
101 | "lib.rs"
102 | "mod.rs"
103 | "index.ts"
104 | "index.js"
105 | "index.tsx"
106 ) {
107 if basename == "lib.rs" || basename == "mod.rs" {
108 return FileRole::Library;
109 }
110 return FileRole::Entry;
111 }
112
113 if path.contains("/parsers/") || path.contains("/parser/") || path.ends_with("/parser.rs") {
115 return FileRole::Parser;
116 }
117 if path.contains("/mcp/")
118 || path.contains("mcp-server")
119 || path.contains("/tools.rs") && path.contains("mcp")
120 {
121 return FileRole::Mcp;
122 }
123 if path.contains("/cli/") || path.contains("/cmd/") || path.contains("/commands/") {
124 return FileRole::Cli;
125 }
126 if path.contains("/components/")
127 || path.contains("/views/")
128 || path.contains("/pages/")
129 || path.ends_with(".tsx")
130 || path.ends_with(".jsx")
131 {
132 return FileRole::View;
133 }
134 if path.contains("/api/")
135 || path.contains("/server/")
136 || path.contains("/handlers/")
137 || path.contains("/routes/")
138 || path.ends_with("/serve.rs")
139 || path.ends_with("/server.rs")
140 {
141 return FileRole::ApiServer;
142 }
143 if path.contains("/config")
144 || path.ends_with("config.rs")
145 || path.ends_with("config.ts")
146 || path.ends_with("settings.py")
147 || path.ends_with("conf.py")
148 {
149 return FileRole::Config;
150 }
151 if path.contains("/models/") || path.contains("/model/") || path.contains("/types/") {
152 return FileRole::Model;
153 }
154 if path.ends_with("graph.rs")
155 || path.ends_with(".sql")
156 || path.contains("/db/")
157 || path.ends_with("queries.rs")
158 || path.ends_with("repository.rs")
159 || path.contains("/dao/")
160 {
161 return FileRole::QueryLayer;
162 }
163 if path.contains("/skill") || path.ends_with("skill.rs") {
164 return FileRole::Skill;
165 }
166 if path.contains("build.rs")
167 || path.contains("/scripts/")
168 || path.contains("/build/")
169 || path.contains(".github/")
170 {
171 return FileRole::Build;
172 }
173
174 if summary.exported_count >= 8 && summary.callers.len() <= 3 {
176 return FileRole::Library;
177 }
178
179 FileRole::Other
180}