package review import ( "encoding/json" "fmt" "strings" "gopkg.in/yaml.v3" "gitea-codex-bot/internal/domain" ) type rawConfig struct { Enabled *bool `yaml:"enabled"` Review struct { DefaultMode string `yaml:"default_mode"` MaxDiffBytes int `yaml:"max_diff_bytes"` IncludeTests bool `yaml:"include_tests"` Focus []string `yaml:"focus"` } `yaml:"review"` Ignore []string `yaml:"ignore"` } func ParseRepoConfig(text string) (domain.RepoReviewConfig, error) { cfg := domain.DefaultRepoReviewConfig() cfg.Configured = true var raw rawConfig if err := yaml.Unmarshal([]byte(text), &raw); err != nil { return domain.RepoReviewConfig{}, err } if raw.Enabled != nil { cfg.Enabled = *raw.Enabled } if raw.Review.DefaultMode != "" { cfg.DefaultMode = strings.ToLower(strings.TrimSpace(raw.Review.DefaultMode)) } if raw.Review.MaxDiffBytes > 0 { cfg.MaxDiffBytes = raw.Review.MaxDiffBytes } cfg.IncludeTests = raw.Review.IncludeTests if raw.Review.Focus != nil { cfg.Focus = boundedStrings(raw.Review.Focus, 32, 200) } cfg.Ignore = boundedStrings(raw.Ignore, 128, 500) return cfg, nil } func MissingRepoConfig() domain.RepoReviewConfig { cfg := domain.DefaultRepoReviewConfig() cfg.Configured = false return cfg } func boundedStrings(input []string, maxItems, maxLen int) []string { out := make([]string, 0, min(len(input), maxItems)) for _, item := range input { item = strings.TrimSpace(item) if item != "" && len(item) <= maxLen { out = append(out, item) } if len(out) == maxItems { break } } return out } func ResolveMode(cmd *domain.ParsedCommand, cfg domain.RepoReviewConfig) { if cmd.Name == "review" && !cmd.ModeExplicit { if cfg.DefaultMode == "full" || cfg.DefaultMode == "summary" || cfg.DefaultMode == "security" || cfg.DefaultMode == "performance" || cfg.DefaultMode == "tests" { cmd.Mode = cfg.DefaultMode } else { cmd.Mode = "summary" } } } func BuildPrompt(cmd domain.ParsedCommand, cfg domain.RepoReviewConfig, pr domain.PullRequestContext) string { raw := strings.TrimSpace(cmd.Raw) intent := raw if at := strings.IndexAny(raw, " \t\r\n"); at >= 0 { intent = strings.TrimSpace(raw[at:]) } if at := strings.IndexAny(intent, " \t\r\n"); at >= 0 { intent = strings.TrimSpace(intent[at:]) } if intent == "" { intent = "review this pull request and report introduced issues." } focus := strings.Join(cfg.Focus, ", ") if focus == "" { focus = "correctness, security, maintainability" } ignore := strings.Join(cfg.Ignore, ", ") if ignore == "" { ignore = "(none)" } tests := "Do not run tests, benchmarks, or other executables. Review changes statically unless explicitly asked." if cmd.Mode == "tests" || cfg.IncludeTests { tests = "Tests may be executed for this run because tests mode/include_tests is explicitly enabled." } return fmt.Sprintf("review: %s\nReview only issues introduced by this PR.\nCompare exactly these commits: base `%s` ... head `%s`.\nUse local git data from this checkout; do not review unrelated history.\nRequested mode: %s.\nFocus areas: %s.\nIgnore patterns: %s.\nInclude tests setting: %t.\n%s\nFull review requested: %t.\nReturn strict JSON matching the provided output schema.", intent, pr.BaseSHA, pr.HeadSHA, cmd.Mode, focus, ignore, cfg.IncludeTests, tests, cmd.Full) } func ValidateResult(result domain.ReviewResult) error { return result.Validate() } func DecodeResult(data []byte) (domain.ReviewResult, error) { var result domain.ReviewResult dec := json.NewDecoder(strings.NewReader(string(data))) dec.DisallowUnknownFields() if err := dec.Decode(&result); err != nil { return domain.ReviewResult{}, err } if err := result.Validate(); err != nil { return domain.ReviewResult{}, err } return result, nil } func FormatQueueAck(sha string) string { return fmt.Sprintf("👀 Codex review queued for commit `%s`.", first(sha, 7)) } func FormatCooldownAck(seconds int) string { return fmt.Sprintf("⏳ Cooldown active. Please wait %ds before requesting another review on this PR.", seconds) } func FormatDisabledAck() string { return "🚫 Review is disabled by `.codex-review.yml` for this repository." } func FormatUnsupportedAck(name string) string { return fmt.Sprintf("⚠️ Command `@codex %s` is not enabled on this repository.", name) } func FormatResultComment(sha string, result domain.ReviewResult, configured bool) string { marker := fmt.Sprintf("", sha) body := strings.TrimSpace(result.MarkdownComment) if body == "" { body = fmt.Sprintf("## Codex Review\n\nVerdict: `%s`\nConfidence: `%.2f`\n\n%s", result.Verdict, result.Confidence, result.Summary) if len(result.Findings) == 0 { body += "\n\nNo blocking issues found." } else { body += "\n\nFindings:" for i, f := range result.Findings { suggestion := "n/a" if f.Suggestion != nil && *f.Suggestion != "" { suggestion = *f.Suggestion } body += fmt.Sprintf("\n\n%d. `%s:%d-%d` (%s)\n %s\n %s\n Suggestion: %s", i+1, f.File, f.LineStart, f.LineEnd, f.Severity, f.Title, f.Body, suggestion) } } } else if len(result.Findings) > 0 { body += "\n\n---\n\n### Structured Findings\n\n" for i, f := range result.Findings { body += fmt.Sprintf("%d. `%s:%d-%d` (%s)\n %s\n %s\n\n", i+1, f.File, f.LineStart, f.LineEnd, f.Severity, f.Title, f.Body) } } if result.Meta != nil && result.Meta.Model != "" { body += fmt.Sprintf("\n_Note: model `%s`, input `%d`, output `%d`, total `%d` tokens used._", result.Meta.Model, result.Meta.Usage.InputTokens, result.Meta.Usage.OutputTokens, result.Meta.Usage.TotalTokens) } if !configured { body += "\n\n> ℹ️.codex-review.yml is not configured" } if strings.HasPrefix(body, "