9e15b73a23
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 15s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 28s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 30s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 51s
The external dependency (goccy/go-yaml) violates the repository's stdlib-only convention (CONVENTIONS.md). While YAML provides better readability for multi-line strings, the convenience doesn't justify breaking a hard rule. Reverts: - External dependency on github.com/goccy/go-yaml - YAML parsing logic in persona.go - YAML persona files (restored as JSON) - YAML-specific tests - Design document (feature rejected) The persona files work fine with JSON. Multi-line strings use \n escapes which is less pretty but acceptable for internal files. This addresses all MAJOR findings from review bots regarding the external dependency violation.
113 lines
3.0 KiB
Go
113 lines
3.0 KiB
Go
package review
|
|
|
|
import (
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
//go:embed personas/*.json
|
|
var embeddedPersonas embed.FS
|
|
|
|
// Persona defines a specialized review role with focused expertise.
|
|
type Persona struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name"`
|
|
ModelPref string `json:"model_preference,omitempty"`
|
|
Identity string `json:"identity"`
|
|
Focus []string `json:"focus"`
|
|
Ignore []string `json:"ignore"`
|
|
Severity Severity `json:"severity"`
|
|
OutputFormat string `json:"output_format,omitempty"`
|
|
}
|
|
|
|
// Severity defines what constitutes each severity level for this persona.
|
|
// These are prompt guidance for the LLM, not output format changes.
|
|
type Severity struct {
|
|
Major string `json:"major"`
|
|
Minor string `json:"minor"`
|
|
Nit string `json:"nit"`
|
|
}
|
|
|
|
// LoadPersona loads a persona from a JSON file path.
|
|
func LoadPersona(path string) (*Persona, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read persona file %s: %w", path, err)
|
|
}
|
|
return parsePersona(data, path)
|
|
}
|
|
|
|
// LoadBuiltinPersona loads a built-in persona by name.
|
|
// Returns an error if the persona doesn't exist.
|
|
func LoadBuiltinPersona(name string) (*Persona, error) {
|
|
filename := name + ".json"
|
|
data, err := embeddedPersonas.ReadFile("personas/" + filename) // embed.FS paths use forward slashes per io/fs spec
|
|
if err != nil {
|
|
available := ListBuiltinPersonas()
|
|
return nil, fmt.Errorf("unknown built-in persona %q (available: %s)", name, strings.Join(available, ", "))
|
|
}
|
|
return parsePersona(data, "builtin:"+name)
|
|
}
|
|
|
|
// ListBuiltinPersonas returns the names of all built-in personas.
|
|
// Returns an empty slice if the embedded directory cannot be read.
|
|
func ListBuiltinPersonas() []string {
|
|
entries, err := embeddedPersonas.ReadDir("personas")
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
var names []string
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
continue
|
|
}
|
|
name := e.Name()
|
|
if strings.HasSuffix(name, ".json") {
|
|
names = append(names, strings.TrimSuffix(name, ".json"))
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
func parsePersona(data []byte, source string) (*Persona, error) {
|
|
var p Persona
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
return nil, fmt.Errorf("parse persona %s: %w", source, err)
|
|
}
|
|
if err := validatePersona(&p, source); err != nil {
|
|
return nil, err
|
|
}
|
|
return &p, nil
|
|
}
|
|
|
|
func validatePersona(p *Persona, source string) error {
|
|
if p.Name == "" {
|
|
return fmt.Errorf("persona %s: name is required", source)
|
|
}
|
|
if p.Identity == "" {
|
|
return fmt.Errorf("persona %s: identity is required", source)
|
|
}
|
|
// DisplayName defaults to Name if not set
|
|
if p.DisplayName == "" {
|
|
p.DisplayName = p.Name
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CapitalizeFirst capitalizes the first rune of a string in a Unicode-safe way.
|
|
// Returns the original string if it's empty.
|
|
func CapitalizeFirst(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
r, size := utf8.DecodeRuneInString(s)
|
|
if r == utf8.RuneError {
|
|
return s
|
|
}
|
|
return strings.ToUpper(string(r)) + s[size:]
|
|
}
|