diff --git a/.grokkit/prompts/go.md b/.grokkit/prompts/go.md index f9f30a7..d56b773 100644 --- a/.grokkit/prompts/go.md +++ b/.grokkit/prompts/go.md @@ -1,31 +1,29 @@ -You are an expert Go educator and code archaeologist. -Analyze this Go project for a developer or hobbyist who wants to deeply understand the codebase. +You are an expert Go educator and codebase archaeologist helping a learning developer or hobbyist deeply understand a project. -Produce a single, well-structured Markdown report with these exact sections: +Generate a single, clean, educational Markdown report with these exact sections (use proper Markdown): -# Project Analysis: {{Project Name}} +# Project Analysis: [Inferred Project Name] ## Tech Stack & Layout -- Primary language, version, build tools -- Key dependencies and why they were chosen -- Directory structure overview +- Language/version, build system, key dependencies and why they were chosen +- High-level directory structure -## Module / Package Relationships -- How packages depend on each other (import graph) -- Public APIs and their purpose +## Module & Package Relationships +- How packages depend on each other +- Main public APIs and their purpose ## Function & Method Reference -For every exported function/method: -- What it does (clear English) -- How it works (key logic, patterns) -- Why it exists (design rationale) +For each exported function/method (group by package): +- What it does +- How it works (key logic, patterns, idioms) +- Why it exists (design rationale or problem it solves) ## Object & Data Flow -- Major structs/types and their relationships -- Any database/ORM mappings if present +- Important structs/types and their relationships +- Any database/ORM mappings or persistence patterns (if present) ## Learning Path & Gotchas -- Recommended reading order -- Common pitfalls for newcomers +- Recommended order to read/understand the code +- Common pitfalls or tricky parts for newcomers -Be educational, encouraging, and precise. Use code snippets only when they clarify. +Be precise, encouraging, and educational. Use short code snippets only when they illuminate a concept. Do not add extra commentary outside the sections. diff --git a/cmd/analyze.go b/cmd/analyze.go index 98547c4..1879402 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -159,18 +159,28 @@ func previewLines(content string, n int) { // buildProjectContext harvests useful context (tree, go.mod, git remote, etc.). // Expand this as needed for other languages. +// buildProjectContext builds a concise but useful context block for the AI. func buildProjectContext(dir string, files []string) string { var sb strings.Builder - sb.WriteString("Project Directory: " + dir + "\n\n") + sb.WriteString("Project Root: " + dir + "\n\n") + sb.WriteString(fmt.Sprintf("Total source files: %d\n\n", len(files))) - // Simple tree summary (or call tree/git ls-files if desired) - sb.WriteString("Source files found: " + fmt.Sprintf("%d\n\n", len(files))) - - // Git info - if remotes, err := git.Run([]string{"remote", "-v"}); err == nil { - sb.WriteString("Git Remotes:\n" + remotes + "\n\n") + // Top-level files only (avoid token explosion) + sb.WriteString("Key files (top level):\n") + for _, f := range files { + rel, _ := filepath.Rel(dir, f) + if strings.Count(rel, string(filepath.Separator)) <= 2 { // shallow + sb.WriteString(" - " + rel + "\n") + } + if sb.Len() > 2500 { + break + } + } + + // Git remotes (if present) + if out, err := git.Run([]string{"remote", "-v"}); err == nil && out != "" { + sb.WriteString("\nGit Remotes:\n" + out + "\n") } - // Add more (go.mod, package.json, Cargo.toml, etc.) here for richer context return sb.String() } diff --git a/internal/linter/language.go b/internal/linter/language.go index f707e34..6d90456 100644 --- a/internal/linter/language.go +++ b/internal/linter/language.go @@ -1,4 +1,55 @@ package linter -// DetectPrimaryLanguage returns the most common language or falls back to "go" -func DetectPrimaryLanguage(files []string) string { ... } \ No newline at end of file +import ( + "path/filepath" + "strings" +) + +// DetectPrimaryLanguage returns the dominant language in the given list of files. +// It counts occurrences of each detected language and returns the most frequent one. +// Falls back to "go" (common default) or "unknown". +func DetectPrimaryLanguage(files []string) string { + if len(files) == 0 { + return "unknown" + } + + counts := make(map[string]int) + for _, file := range files { + lang, err := DetectLanguage(file) + if err == nil && lang != nil { + name := strings.ToLower(lang.Name) + counts[name]++ + } + } + + if len(counts) == 0 { + return "unknown" + } + + // Find the language with the highest count + var bestLang string + maxCount := -1 + for lang, count := range counts { + if count > maxCount { + maxCount = count + bestLang = lang + } + } + + // Friendly bias toward Go in mixed repos (common case) + if counts["go"] > 0 && counts["go"] >= maxCount/2 { + return "go" + } + + return bestLang +} + +// SupportedLanguages returns a list of all languages known to the linter. +// Useful for error messages or future prompt discovery. +func SupportedLanguages() []string { + var langs []string + for _, l := range languages { + langs = append(langs, strings.ToLower(l.Name)) + } + return langs +} diff --git a/internal/prompts/analyze.go b/internal/prompts/analyze.go index bda777f..1bdb2f1 100644 --- a/internal/prompts/analyze.go +++ b/internal/prompts/analyze.go @@ -3,29 +3,37 @@ package prompts import ( "os" "path/filepath" + "strings" ) -// LoadAnalysisPrompt returns the path and content of the best matching prompt. -// Search order: {projectRoot}/.grokkit/prompts/{language}.md → ~/.config/grokkit/prompts/{language}.md +// LoadAnalysisPrompt locates and reads a language-specific analysis prompt. +// Search order (developer-first, project-local preferred): +// 1. {projectRoot}/.grokkit/prompts/{language}.md +// 2. ~/.config/grokkit/prompts/{language}.md +// +// Returns os.ErrNotExist if none found so the command can give a clear, actionable error. func LoadAnalysisPrompt(projectRoot, language string) (path, content string, err error) { - if language == "" { - language = "unknown" + if language == "" || language == "unknown" { + language = "go" + } + language = strings.ToLower(language) + + // 1. Project-local (preferred for per-project customization) + local := filepath.Join(projectRoot, ".grokkit", "prompts", language+".md") + if data, readErr := os.ReadFile(local); readErr == nil { + return local, string(data), nil } - // 1. Project-local (preferred) - localPath := filepath.Join(projectRoot, ".grokkit", "prompts", language+".md") - if data, readErr := os.ReadFile(localPath); readErr == nil { - return localPath, string(data), nil - } - - // 2. Global config + // 2. Global fallback home := os.Getenv("HOME") if home == "" { - home = "/root" // fallback for containers + home = os.Getenv("USERPROFILE") // Windows } - globalPath := filepath.Join(home, ".config", "grokkit", "prompts", language+".md") - if data, readErr := os.ReadFile(globalPath); readErr == nil { - return globalPath, string(data), nil + if home != "" { + global := filepath.Join(home, ".config", "grokkit", "prompts", language+".md") + if data, readErr := os.ReadFile(global); readErr == nil { + return global, string(data), nil + } } return "", "", os.ErrNotExist