package review import ( "encoding/json" "fmt" "strings" ) // Finding represents a single code review finding. type Finding struct { Severity string `json:"severity"` File string `json:"file"` Line int `json:"line"` Finding string `json:"finding"` } // ReviewResult is the structured output from the LLM. type ReviewResult struct { Verdict string `json:"verdict"` Summary string `json:"summary"` Findings []Finding `json:"findings"` Recommendation string `json:"recommendation"` } // ParseResponse parses the LLM response into a ReviewResult. func ParseResponse(response string) (*ReviewResult, error) { // Try to extract JSON from the response — the LLM might wrap it in markdown fences cleaned := extractJSON(response) var result ReviewResult if err := json.Unmarshal([]byte(cleaned), &result); err != nil { return nil, fmt.Errorf("parse LLM response as JSON: %w\nRaw response: %s", err, response) } // Validate verdict switch result.Verdict { case "APPROVE", "REQUEST_CHANGES": // valid default: return nil, fmt.Errorf("invalid verdict %q (must be APPROVE or REQUEST_CHANGES)", result.Verdict) } // Validate finding severities for i, f := range result.Findings { switch f.Severity { case "MAJOR", "MINOR", "NIT": // valid default: return nil, fmt.Errorf("finding %d has invalid severity %q", i, f.Severity) } } return &result, nil } // extractJSON attempts to pull JSON from a potentially markdown-wrapped response. func extractJSON(s string) string { s = strings.TrimSpace(s) // Remove markdown code fences if present if strings.HasPrefix(s, "```") { lines := strings.Split(s, "\n") // Remove first line (```json or ```) if len(lines) > 2 { lines = lines[1:] } // Remove last line (```) if len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "```" { lines = lines[:len(lines)-1] } s = strings.Join(lines, "\n") } s = strings.TrimSpace(s) return s }