67d835909f
Adds a budget package that estimates token usage and progressively trims context to fit within model-specific limits. Trim order (least important first): 1. Language patterns 2. Repository conventions 3. Full file context 4. Diff (truncated as last resort) When content is trimmed, a note is appended to the user prompt so the LLM knows context was reduced. - New budget package with Fit(), EstimateTokens(), LimitForModel() - Model limit table (GPT-4.1: 128K, GPT-5: 200K, Claude: 200K) - Refactored review/prompt.go: BuildSystemBase() and BuildUserMeta() extract non-trimmable content; old functions delegate to new ones - main.go uses budget.Fit() instead of direct prompt assembly - 7 unit tests covering all trim paths Closes #19
111 lines
4.6 KiB
Go
111 lines
4.6 KiB
Go
// Package review builds prompts for AI code review and parses LLM responses
|
|
// into structured review results.
|
|
package review
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// BuildSystemBase returns the core system prompt instructions without
|
|
// patterns or conventions. Used by the budget package to separate
|
|
// trimmable from non-trimmable content.
|
|
func BuildSystemBase() string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("You are an expert code reviewer. Review the provided pull request diff carefully.\n\n")
|
|
sb.WriteString("CONTEXT:\n")
|
|
sb.WriteString("- You will receive the full content of modified files for reference, followed by the diff showing what changed.\n")
|
|
sb.WriteString("- The diff shows ONLY what was added/removed. The full file content provides complete context.\n")
|
|
sb.WriteString("- Focus your review on the CHANGES (the diff), using the full files for context.\n\n")
|
|
sb.WriteString("Your task:\n")
|
|
sb.WriteString("1. Review the diff for correctness, idiomatic code, potential bugs, and design issues.\n")
|
|
sb.WriteString("2. Consider the CI status — if CI has failed, that is an automatic REQUEST_CHANGES regardless of code quality.\n")
|
|
sb.WriteString("3. Output your review as structured JSON (and ONLY JSON, no markdown fences or other text).\n\n")
|
|
sb.WriteString("Output format:\n")
|
|
sb.WriteString("{\n")
|
|
sb.WriteString(" \"verdict\": \"APPROVE\" or \"REQUEST_CHANGES\",\n")
|
|
sb.WriteString(" \"summary\": \"Brief overall assessment (1-3 sentences)\",\n")
|
|
sb.WriteString(" \"findings\": [\n")
|
|
sb.WriteString(" {\n")
|
|
sb.WriteString(" \"severity\": \"MAJOR\" or \"MINOR\" or \"NIT\",\n")
|
|
sb.WriteString(" \"file\": \"path/to/file\",\n")
|
|
sb.WriteString(" \"line\": <line number from the diff>,\n")
|
|
sb.WriteString(" \"finding\": \"Description of the issue\"\n")
|
|
sb.WriteString(" }\n")
|
|
sb.WriteString(" ],\n")
|
|
sb.WriteString(" \"recommendation\": \"Full recommendation text explaining your verdict\"\n")
|
|
sb.WriteString("}\n\n")
|
|
sb.WriteString("Rules:\n")
|
|
sb.WriteString("- If there are any MAJOR findings → verdict must be REQUEST_CHANGES\n")
|
|
sb.WriteString("- If there are no MAJOR findings → verdict should be APPROVE\n")
|
|
sb.WriteString("- If CI has failed → verdict must be REQUEST_CHANGES with a finding noting the CI failure\n")
|
|
sb.WriteString("- Be thorough but fair. Don't nitpick style unless it impacts readability significantly.\n")
|
|
sb.WriteString("- Line numbers should reference the new file line numbers from the diff headers.\n")
|
|
sb.WriteString("- If the diff is empty or trivial (only formatting/whitespace), APPROVE with no findings.\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// BuildSystemPrompt constructs the full system prompt with patterns and conventions.
|
|
// Deprecated: Use BuildSystemBase with budget.Fit for context-aware assembly.
|
|
func BuildSystemPrompt(conventions, patterns string) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(BuildSystemBase())
|
|
|
|
if patterns != "" {
|
|
sb.WriteString(fmt.Sprintf("\n\n## Language Patterns & Idioms\n\nUse the following patterns as review criteria. Code that violates these established patterns is a finding:\n\n%s\n", patterns))
|
|
}
|
|
|
|
if conventions != "" {
|
|
sb.WriteString(fmt.Sprintf("\n\n## Repository Conventions\n\nThe repository has the following coding conventions that must be respected:\n\n%s\n", conventions))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// BuildUserMeta returns the PR metadata header (title, description, CI status)
|
|
// without the diff or file context. Used by the budget package.
|
|
func BuildUserMeta(title, description string, ciPassed bool, ciDetails string) string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(fmt.Sprintf("## Pull Request: %s\n\n", title))
|
|
|
|
if description != "" {
|
|
sb.WriteString(fmt.Sprintf("### Description\n%s\n\n", description))
|
|
}
|
|
|
|
ciStatus := "PASSED"
|
|
if !ciPassed {
|
|
ciStatus = "FAILED"
|
|
}
|
|
sb.WriteString(fmt.Sprintf("### CI Status: %s\n", ciStatus))
|
|
|
|
if ciDetails != "" {
|
|
sb.WriteString(fmt.Sprintf("CI Details: %s\n", ciDetails))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// BuildUserPrompt constructs the user message with PR context.
|
|
// Deprecated: Use BuildUserMeta with budget.Fit for context-aware assembly.
|
|
func BuildUserPrompt(title, description, diff, fileContext string, ciPassed bool, ciDetails string) string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(BuildUserMeta(title, description, ciPassed, ciDetails))
|
|
|
|
if fileContext != "" {
|
|
sb.WriteString("\n### Full File Context (modified files)\n\n")
|
|
sb.WriteString(fileContext)
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
sb.WriteString("\n### Diff (changes to review)\n\n")
|
|
sb.WriteString("```diff\n")
|
|
sb.WriteString(diff)
|
|
sb.WriteString("\n```\n")
|
|
|
|
return sb.String()
|
|
}
|