Skip to content

Commit f090f4c

Browse files
authored
Merge pull request #26 from Priyans-hu/feat/phase3-improvements
feat: Phase 3 improvements - Command prioritization, ML detection, Project type inference
2 parents 3d30ca7 + 775dfd2 commit f090f4c

File tree

9 files changed

+1643
-112
lines changed

9 files changed

+1643
-112
lines changed

internal/analyzer/analyzer.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ func (a *Analyzer) Analyze() (*types.Analysis, error) {
131131
codePatternDetector := detector.NewCodePatternDetector(absPath, files)
132132
analysis.CodePatterns = codePatternDetector.Detect()
133133

134+
// ML-specific pattern detection
135+
mlDetector := detector.NewMLDetector(absPath, files)
136+
if mlPatterns := mlDetector.GetMLPatterns(); len(mlPatterns) > 0 {
137+
analysis.CodePatterns.MLPatterns = mlPatterns
138+
}
139+
134140
// Detect git conventions (commit messages, branch naming) - using go-git library
135141
gitDetector := detector.NewGitDetectorGoGit(absPath)
136142
analysis.GitConventions = gitDetector.Detect()

internal/analyzer/parallel.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ func (pa *ParallelAnalyzer) runPhase2(files []types.FileInfo, analysis *types.An
233233
defer wg.Done()
234234
codePatternDetector := detector.NewCodePatternDetector(pa.rootPath, files)
235235
patterns := codePatternDetector.Detect()
236+
237+
// Add ML-specific patterns
238+
mlDetector := detector.NewMLDetector(pa.rootPath, files)
239+
if mlPatterns := mlDetector.GetMLPatterns(); len(mlPatterns) > 0 {
240+
patterns.MLPatterns = mlPatterns
241+
}
242+
236243
mu.Lock()
237244
analysis.CodePatterns = patterns
238245
mu.Unlock()
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
package detector
2+
3+
import (
4+
"regexp"
5+
"sort"
6+
"strings"
7+
8+
"github.com/Priyans-hu/argus/pkg/types"
9+
)
10+
11+
// CommandCategory represents a category of commands with priority
12+
type CommandCategory int
13+
14+
const (
15+
CategoryBuild CommandCategory = iota
16+
CategoryTest
17+
CategoryLint
18+
CategoryFormat
19+
CategoryRun
20+
CategoryInstall
21+
CategoryClean
22+
CategoryGenerate
23+
CategoryDeploy
24+
CategoryDocker
25+
CategoryDatabase
26+
CategoryOther
27+
)
28+
29+
// categoryPriority maps categories to their priority (lower = higher priority)
30+
var categoryPriority = map[CommandCategory]int{
31+
CategoryBuild: 1,
32+
CategoryTest: 2,
33+
CategoryLint: 3,
34+
CategoryFormat: 4,
35+
CategoryRun: 5,
36+
CategoryInstall: 6,
37+
CategoryClean: 7,
38+
CategoryGenerate: 8,
39+
CategoryDeploy: 9,
40+
CategoryDocker: 10,
41+
CategoryDatabase: 11,
42+
CategoryOther: 99,
43+
}
44+
45+
// categoryPatterns maps regex patterns to categories
46+
// These work across all languages
47+
var categoryPatterns = []struct {
48+
pattern *regexp.Regexp
49+
category CommandCategory
50+
}{
51+
// Build commands
52+
{regexp.MustCompile(`(?i)^(make\s+)?(build|compile|dist|release|bundle)$`), CategoryBuild},
53+
{regexp.MustCompile(`(?i)^go\s+build`), CategoryBuild},
54+
{regexp.MustCompile(`(?i)^cargo\s+build`), CategoryBuild},
55+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?build`), CategoryBuild},
56+
{regexp.MustCompile(`(?i)^python\s+setup\.py\s+build`), CategoryBuild},
57+
{regexp.MustCompile(`(?i)^poetry\s+build`), CategoryBuild},
58+
{regexp.MustCompile(`(?i)^gradle\s+build`), CategoryBuild},
59+
{regexp.MustCompile(`(?i)^mvn\s+(compile|package)`), CategoryBuild},
60+
{regexp.MustCompile(`(?i)^dotnet\s+build`), CategoryBuild},
61+
62+
// Test commands
63+
{regexp.MustCompile(`(?i)^(make\s+)?test`), CategoryTest},
64+
{regexp.MustCompile(`(?i)^go\s+test`), CategoryTest},
65+
{regexp.MustCompile(`(?i)^cargo\s+test`), CategoryTest},
66+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?test`), CategoryTest},
67+
{regexp.MustCompile(`(?i)^pytest`), CategoryTest},
68+
{regexp.MustCompile(`(?i)^python\s+-m\s+(pytest|unittest)`), CategoryTest},
69+
{regexp.MustCompile(`(?i)^poetry\s+run\s+(pytest|python\s+-m\s+pytest)`), CategoryTest},
70+
{regexp.MustCompile(`(?i)^jest`), CategoryTest},
71+
{regexp.MustCompile(`(?i)^vitest`), CategoryTest},
72+
{regexp.MustCompile(`(?i)^gradle\s+test`), CategoryTest},
73+
{regexp.MustCompile(`(?i)^mvn\s+test`), CategoryTest},
74+
{regexp.MustCompile(`(?i)^dotnet\s+test`), CategoryTest},
75+
{regexp.MustCompile(`(?i)^rspec`), CategoryTest},
76+
{regexp.MustCompile(`(?i)^bundle\s+exec\s+rspec`), CategoryTest},
77+
{regexp.MustCompile(`(?i)coverage`), CategoryTest},
78+
79+
// Lint commands
80+
{regexp.MustCompile(`(?i)^(make\s+)?lint`), CategoryLint},
81+
{regexp.MustCompile(`(?i)^golangci-lint`), CategoryLint},
82+
{regexp.MustCompile(`(?i)^cargo\s+clippy`), CategoryLint},
83+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?lint`), CategoryLint},
84+
{regexp.MustCompile(`(?i)^eslint`), CategoryLint},
85+
{regexp.MustCompile(`(?i)^(ruff|flake8|pylint|mypy)\s+check`), CategoryLint},
86+
{regexp.MustCompile(`(?i)^poetry\s+run\s+(ruff|flake8|pylint|mypy)`), CategoryLint},
87+
{regexp.MustCompile(`(?i)^rubocop`), CategoryLint},
88+
{regexp.MustCompile(`(?i)^check`), CategoryLint},
89+
90+
// Format commands
91+
{regexp.MustCompile(`(?i)^(make\s+)?(format|fmt)$`), CategoryFormat},
92+
{regexp.MustCompile(`(?i)^go\s+fmt`), CategoryFormat},
93+
{regexp.MustCompile(`(?i)^gofmt`), CategoryFormat},
94+
{regexp.MustCompile(`(?i)^goimports`), CategoryFormat},
95+
{regexp.MustCompile(`(?i)^cargo\s+fmt`), CategoryFormat},
96+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?format`), CategoryFormat},
97+
{regexp.MustCompile(`(?i)^prettier`), CategoryFormat},
98+
{regexp.MustCompile(`(?i)^(black|ruff\s+format|autopep8|yapf)`), CategoryFormat},
99+
{regexp.MustCompile(`(?i)^poetry\s+run\s+(black|ruff\s+format)`), CategoryFormat},
100+
101+
// Run/Dev commands
102+
{regexp.MustCompile(`(?i)^(make\s+)?(run|start|serve|dev)$`), CategoryRun},
103+
{regexp.MustCompile(`(?i)^go\s+run`), CategoryRun},
104+
{regexp.MustCompile(`(?i)^cargo\s+run`), CategoryRun},
105+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?(start|dev|serve)`), CategoryRun},
106+
{regexp.MustCompile(`(?i)^python\s+(app|main|run|manage)\.py`), CategoryRun},
107+
{regexp.MustCompile(`(?i)^(flask|uvicorn|gunicorn|django)`), CategoryRun},
108+
{regexp.MustCompile(`(?i)^poetry\s+run\s+(python|flask|uvicorn)`), CategoryRun},
109+
{regexp.MustCompile(`(?i)^rails\s+s`), CategoryRun},
110+
{regexp.MustCompile(`(?i)^bundle\s+exec\s+rails`), CategoryRun},
111+
{regexp.MustCompile(`(?i)runserver`), CategoryRun},
112+
113+
// Install commands
114+
{regexp.MustCompile(`(?i)^(make\s+)?install$`), CategoryInstall},
115+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+install`), CategoryInstall},
116+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+ci`), CategoryInstall},
117+
{regexp.MustCompile(`(?i)^pip\s+install`), CategoryInstall},
118+
{regexp.MustCompile(`(?i)^poetry\s+install`), CategoryInstall},
119+
{regexp.MustCompile(`(?i)^cargo\s+install`), CategoryInstall},
120+
{regexp.MustCompile(`(?i)^bundle\s+install`), CategoryInstall},
121+
{regexp.MustCompile(`(?i)^go\s+mod\s+(download|tidy)`), CategoryInstall},
122+
{regexp.MustCompile(`(?i)^setup`), CategoryInstall},
123+
124+
// Clean commands
125+
{regexp.MustCompile(`(?i)^(make\s+)?clean`), CategoryClean},
126+
{regexp.MustCompile(`(?i)^cargo\s+clean`), CategoryClean},
127+
{regexp.MustCompile(`(?i)^go\s+clean`), CategoryClean},
128+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?clean`), CategoryClean},
129+
130+
// Generate commands
131+
{regexp.MustCompile(`(?i)^(make\s+)?(generate|gen|codegen|proto)`), CategoryGenerate},
132+
{regexp.MustCompile(`(?i)^go\s+generate`), CategoryGenerate},
133+
{regexp.MustCompile(`(?i)^protoc`), CategoryGenerate},
134+
135+
// Deploy commands
136+
{regexp.MustCompile(`(?i)^(make\s+)?deploy`), CategoryDeploy},
137+
{regexp.MustCompile(`(?i)^(npm|yarn|pnpm|bun)\s+(run\s+)?deploy`), CategoryDeploy},
138+
{regexp.MustCompile(`(?i)^kubectl`), CategoryDeploy},
139+
{regexp.MustCompile(`(?i)^helm`), CategoryDeploy},
140+
{regexp.MustCompile(`(?i)^terraform`), CategoryDeploy},
141+
142+
// Docker commands
143+
{regexp.MustCompile(`(?i)^(make\s+)?docker`), CategoryDocker},
144+
{regexp.MustCompile(`(?i)^docker\s+(build|compose|run)`), CategoryDocker},
145+
146+
// Database commands
147+
{regexp.MustCompile(`(?i)^(make\s+)?migrate`), CategoryDatabase},
148+
{regexp.MustCompile(`(?i)^(make\s+)?seed`), CategoryDatabase},
149+
{regexp.MustCompile(`(?i)migrations`), CategoryDatabase},
150+
{regexp.MustCompile(`(?i)^(npx\s+)?prisma`), CategoryDatabase},
151+
{regexp.MustCompile(`(?i)^alembic`), CategoryDatabase},
152+
{regexp.MustCompile(`(?i)^rails\s+db:`), CategoryDatabase},
153+
}
154+
155+
// PrioritizedCommand extends Command with priority info
156+
type PrioritizedCommand struct {
157+
types.Command
158+
Category CommandCategory
159+
Priority int
160+
}
161+
162+
// categorizeCommand determines the category of a command
163+
func categorizeCommand(cmd types.Command) CommandCategory {
164+
// Check command name against patterns
165+
cmdName := strings.TrimSpace(cmd.Name)
166+
167+
for _, cp := range categoryPatterns {
168+
if cp.pattern.MatchString(cmdName) {
169+
return cp.category
170+
}
171+
}
172+
173+
// Check description for hints
174+
desc := strings.ToLower(cmd.Description)
175+
if strings.Contains(desc, "build") || strings.Contains(desc, "compile") {
176+
return CategoryBuild
177+
}
178+
if strings.Contains(desc, "test") {
179+
return CategoryTest
180+
}
181+
if strings.Contains(desc, "lint") || strings.Contains(desc, "check") {
182+
return CategoryLint
183+
}
184+
if strings.Contains(desc, "format") {
185+
return CategoryFormat
186+
}
187+
if strings.Contains(desc, "install") || strings.Contains(desc, "dependencies") {
188+
return CategoryInstall
189+
}
190+
if strings.Contains(desc, "clean") {
191+
return CategoryClean
192+
}
193+
if strings.Contains(desc, "run") || strings.Contains(desc, "start") || strings.Contains(desc, "dev") {
194+
return CategoryRun
195+
}
196+
197+
return CategoryOther
198+
}
199+
200+
// PrioritizeCommands sorts commands by category priority and removes duplicates
201+
func PrioritizeCommands(commands []types.Command) []types.Command {
202+
if len(commands) == 0 {
203+
return commands
204+
}
205+
206+
// Categorize all commands
207+
prioritized := make([]PrioritizedCommand, 0, len(commands))
208+
for _, cmd := range commands {
209+
cat := categorizeCommand(cmd)
210+
prioritized = append(prioritized, PrioritizedCommand{
211+
Command: cmd,
212+
Category: cat,
213+
Priority: categoryPriority[cat],
214+
})
215+
}
216+
217+
// Sort by priority, then alphabetically within same priority
218+
sort.SliceStable(prioritized, func(i, j int) bool {
219+
if prioritized[i].Priority != prioritized[j].Priority {
220+
return prioritized[i].Priority < prioritized[j].Priority
221+
}
222+
return prioritized[i].Name < prioritized[j].Name
223+
})
224+
225+
// Remove duplicates (keep first occurrence which has higher priority)
226+
seen := make(map[string]bool)
227+
result := make([]types.Command, 0, len(prioritized))
228+
229+
for _, pc := range prioritized {
230+
// Normalize command for dedup
231+
normalized := normalizeCommand(pc.Name)
232+
if seen[normalized] {
233+
continue
234+
}
235+
seen[normalized] = true
236+
result = append(result, pc.Command)
237+
}
238+
239+
return result
240+
}
241+
242+
// GetQuickReferenceCommands returns the top N most important commands
243+
func GetQuickReferenceCommands(commands []types.Command, maxCommands int) []types.Command {
244+
prioritized := PrioritizeCommands(commands)
245+
246+
// Take top N commands, ensuring we get diversity across categories
247+
if len(prioritized) <= maxCommands {
248+
return prioritized
249+
}
250+
251+
// Ensure at least one from each important category if available
252+
result := make([]types.Command, 0, maxCommands)
253+
categoryCount := make(map[CommandCategory]int)
254+
importantCategories := []CommandCategory{
255+
CategoryBuild, CategoryTest, CategoryLint, CategoryFormat, CategoryRun, CategoryInstall,
256+
}
257+
258+
// First pass: get one from each important category
259+
for _, cmd := range prioritized {
260+
cat := categorizeCommand(cmd)
261+
for _, ic := range importantCategories {
262+
if cat == ic && categoryCount[cat] == 0 {
263+
result = append(result, cmd)
264+
categoryCount[cat]++
265+
break
266+
}
267+
}
268+
if len(result) >= maxCommands {
269+
break
270+
}
271+
}
272+
273+
// Second pass: fill remaining slots with highest priority commands not yet added
274+
added := make(map[string]bool)
275+
for _, cmd := range result {
276+
added[normalizeCommand(cmd.Name)] = true
277+
}
278+
279+
for _, cmd := range prioritized {
280+
if len(result) >= maxCommands {
281+
break
282+
}
283+
normalized := normalizeCommand(cmd.Name)
284+
if !added[normalized] {
285+
result = append(result, cmd)
286+
added[normalized] = true
287+
}
288+
}
289+
290+
return result
291+
}
292+
293+
// normalizeCommand normalizes a command string for deduplication
294+
func normalizeCommand(cmd string) string {
295+
// Remove common variations
296+
cmd = strings.ToLower(cmd)
297+
cmd = strings.TrimSpace(cmd)
298+
299+
// Remove "(in subdir)" suffixes
300+
if idx := strings.Index(cmd, " (in "); idx > 0 {
301+
cmd = cmd[:idx]
302+
}
303+
304+
// Normalize package managers
305+
cmd = strings.ReplaceAll(cmd, "yarn ", "npm ")
306+
cmd = strings.ReplaceAll(cmd, "pnpm ", "npm ")
307+
cmd = strings.ReplaceAll(cmd, "bun ", "npm ")
308+
309+
// Remove "run" from npm run commands
310+
cmd = strings.ReplaceAll(cmd, "npm run ", "npm ")
311+
312+
return cmd
313+
}
314+
315+
// GetCategoryName returns a human-readable category name
316+
func GetCategoryName(cat CommandCategory) string {
317+
names := map[CommandCategory]string{
318+
CategoryBuild: "Build",
319+
CategoryTest: "Test",
320+
CategoryLint: "Lint",
321+
CategoryFormat: "Format",
322+
CategoryRun: "Run",
323+
CategoryInstall: "Setup",
324+
CategoryClean: "Clean",
325+
CategoryGenerate: "Generate",
326+
CategoryDeploy: "Deploy",
327+
CategoryDocker: "Docker",
328+
CategoryDatabase: "Database",
329+
CategoryOther: "Other",
330+
}
331+
if name, ok := names[cat]; ok {
332+
return name
333+
}
334+
return "Other"
335+
}
336+
337+
// GroupCommandsByCategory groups commands by their category
338+
func GroupCommandsByCategory(commands []types.Command) map[string][]types.Command {
339+
groups := make(map[string][]types.Command)
340+
341+
for _, cmd := range commands {
342+
cat := categorizeCommand(cmd)
343+
catName := GetCategoryName(cat)
344+
groups[catName] = append(groups[catName], cmd)
345+
}
346+
347+
return groups
348+
}

0 commit comments

Comments
 (0)