diff options
| author | Paul Buetow <paul@buetow.org> | 2025-08-31 16:18:46 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-08-31 16:18:46 +0300 |
| commit | 66ee5a0e77599a6c9a77ed34a87d5960f364f7e7 (patch) | |
| tree | e3f32cdbc19e6582b3af348ad3b10609c9fb3d71 | |
| parent | 45e9f23077ee8270542370e0c2307aa9dbecf63a (diff) | |
chore(repo): snapshot
| -rw-r--r-- | internal/showcase/ai_context.go | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/internal/showcase/ai_context.go b/internal/showcase/ai_context.go new file mode 100644 index 0000000..f418894 --- /dev/null +++ b/internal/showcase/ai_context.go @@ -0,0 +1,184 @@ +package showcase + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" +) + +// buildAIInputContext prepares a textual context for AI tools when no README exists. +// It returns a string to be piped to the AI tool's stdin and a boolean indicating +// whether this was sourced from an actual README (true) or synthesized (false). +func buildAIInputContext(repoPath string) (string, bool) { + // 1) Try to load a README first + readmeFiles := []string{ + "README.md", "readme.md", "Readme.md", + "README.MD", "README.txt", "readme.txt", + "README", "readme", + } + for _, f := range readmeFiles { + p := filepath.Join(repoPath, f) + if b, err := os.ReadFile(p); err == nil { + return string(b), true + } + } + + // 2) No README: synthesize compact context + var sb strings.Builder + + // File tree (depth-limited) + sb.WriteString("[CONTEXT]\n") + sb.WriteString("Repository does not contain a README.\n") + sb.WriteString("The following is a compact file tree and key manifests/snippets.\n\n") + + sb.WriteString("FILE TREE (depth 2):\n") + tree := listFileTree(repoPath, 2, 200) + for _, line := range tree { + sb.WriteString("- ") + sb.WriteString(line) + sb.WriteString("\n") + } + sb.WriteString("\n") + + // Key manifests we often care about + manifests := []string{ + "go.mod", "go.sum", "package.json", "Cargo.toml", "Cargo.lock", + "pyproject.toml", "requirements.txt", "Makefile", "Dockerfile", + "build.gradle", "pom.xml", "composer.json", + } + wroteHeader := false + for _, m := range manifests { + p := filepath.Join(repoPath, m) + if b, err := os.ReadFile(p); err == nil { + if !wroteHeader { + sb.WriteString("KEY MANIFESTS:\n") + wroteHeader = true + } + sb.WriteString(fmt.Sprintf("--- %s ---\n", m)) + sb.WriteString(trimTo(string(b), 2000)) + sb.WriteString("\n\n") + } + } + + // Source hints: capture first main-ish entry file snippets + // Priority: Go main, Rust main, Node entry, Python main + candidates := []string{ + "cmd", // Go convention + "main.go", + "cmd/main.go", + "src/main.rs", + "index.js", + "src/index.js", + "main.py", + "src/main.py", + } + wroteSrc := false + for _, c := range candidates { + p := filepath.Join(repoPath, c) + info, err := os.Stat(p) + if err != nil { + continue + } + if info.IsDir() { + // collect a few go files under cmd/*/main.go + if c == "cmd" { + _ = filepath.WalkDir(p, func(path string, d fs.DirEntry, err error) error { + if err != nil { return nil } + if d.IsDir() { return nil } + base := filepath.Base(path) + if base == "main.go" { + if b, e := os.ReadFile(path); e == nil { + if !wroteSrc { sb.WriteString("PRIMARY SOURCE SNIPPETS:\n"); wroteSrc = true } + rel, _ := filepath.Rel(repoPath, path) + sb.WriteString(fmt.Sprintf("--- %s ---\n", rel)) + sb.WriteString(trimTo(string(b), 2000)) + sb.WriteString("\n\n") + } + } + return nil + }) + } + continue + } + if b, e := os.ReadFile(p); e == nil { + if !wroteSrc { sb.WriteString("PRIMARY SOURCE SNIPPETS:\n"); wroteSrc = true } + rel, _ := filepath.Rel(repoPath, p) + sb.WriteString(fmt.Sprintf("--- %s ---\n", rel)) + sb.WriteString(trimTo(string(b), 2000)) + sb.WriteString("\n\n") + } + } + + // Fallback: include a few top-level .go, .rs, .py, .js files if we still have nothing + if !wroteSrc { + topFiles := listTopFiles(repoPath, []string{".go", ".rs", ".py", ".js", ".ts", ".tsx"}, 5) + for _, f := range topFiles { + if b, e := os.ReadFile(filepath.Join(repoPath, f)); e == nil { + if !wroteSrc { sb.WriteString("PRIMARY SOURCE SNIPPETS:\n"); wroteSrc = true } + sb.WriteString(fmt.Sprintf("--- %s ---\n", f)) + sb.WriteString(trimTo(string(b), 2000)) + sb.WriteString("\n\n") + } + } + } + + // Instruction to the model + sb.WriteString("[TASK]\n") + sb.WriteString("Summarize this project in 1–2 paragraphs: what it does, why it's useful, and how it's implemented. Mention notable tech choices. Be concise and informative.\n") + + return sb.String(), false +} + +// listFileTree returns a sorted list of relative paths up to a given depth and limit. +func listFileTree(root string, maxDepth int, maxEntries int) []string { + var entries []string + var count int + _ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { return nil } + if path == root { return nil } + rel, e := filepath.Rel(root, path) + if e != nil { return nil } + // depth check + depth := 1 + strings.Count(rel, string(os.PathSeparator)) + if depth > maxDepth { return fs.SkipDir } + entries = append(entries, rel) + count++ + if count >= maxEntries { return fs.SkipDir } + return nil + }) + sort.Strings(entries) + if len(entries) > maxEntries { + entries = entries[:maxEntries] + } + return entries +} + +// listTopFiles lists top-level files with certain extensions up to a limit. +func listTopFiles(root string, exts []string, limit int) []string { + dir, err := os.ReadDir(root) + if err != nil { return nil } + var out []string + for _, e := range dir { + if e.IsDir() { continue } + name := e.Name() + for _, x := range exts { + if strings.HasSuffix(strings.ToLower(name), strings.ToLower(x)) { + out = append(out, name) + break + } + } + if len(out) >= limit { break } + } + sort.Strings(out) + return out +} + +// trimTo soft-limits content length for inclusion in AI context. +func trimTo(s string, max int) string { + if len(s) <= max { return s } + return s[:max] + "\n... [truncated]" +} + |
