fix: address review feedback on persona feature
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 15s
CI / review (/anthropic/v1, anthropic--claude-4.6-sonnet, sonnet, anthropic, SONNET_REVIEW_TOKEN) (pull_request) Successful in 43s
CI / review (/openai/v1, gpt-5, gpt, openai, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m28s
CI / review (/openai/v1, gpt-5, security, openai, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m55s

MAJOR fixes:
- Remove external YAML dependency (github.com/goccy/go-yaml)
  Per project convention: Go standard library only, zero dependencies.
  Convert all persona files from YAML to JSON format.
- Fix TestValidateWorkspacePath error expectation
  Go 1.21+ filepath.Join normalizes absolute paths differently.

MINOR fixes:
- Remove custom contains helper in persona_test.go (use strings.Contains)
- Add Unicode-safe CapitalizeFirst function for header titles
- ListBuiltinPersonas returns empty slice instead of nil on error
- Fix test comment about filepath.Join behavior

Documentation:
- Update README to reflect JSON-only persona format
- Update design doc with note about JSON decision
- Fix action.yml description for persona-file input
This commit is contained in:
Rodin
2026-05-10 10:01:34 -07:00
parent 57e62a345f
commit 4dd67742f9
15 changed files with 215 additions and 250 deletions
+34 -36
View File
@@ -5,37 +5,34 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/goccy/go-yaml"
"unicode/utf8"
)
//go:embed personas/*.yaml
//go:embed personas/*.json
var embeddedPersonas embed.FS
// Persona defines a specialized review role with focused expertise.
type Persona struct {
Name string `json:"name" yaml:"name"`
DisplayName string `json:"display_name" yaml:"display_name"`
ModelPref string `json:"model_preference,omitempty" yaml:"model_preference,omitempty"`
Identity string `json:"identity" yaml:"identity"`
Focus []string `json:"focus" yaml:"focus"`
Ignore []string `json:"ignore" yaml:"ignore"`
Severity Severity `json:"severity" yaml:"severity"`
OutputFormat string `json:"output_format,omitempty" yaml:"output_format,omitempty"`
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" yaml:"major"`
Minor string `json:"minor" yaml:"minor"`
Nit string `json:"nit" yaml:"nit"`
Major string `json:"major"`
Minor string `json:"minor"`
Nit string `json:"nit"`
}
// LoadPersona loads a persona from a file path.
// Supports both YAML (.yaml, .yml) and JSON (.json) formats.
// LoadPersona loads a persona from a JSON file path.
func LoadPersona(path string) (*Persona, error) {
data, err := os.ReadFile(path)
if err != nil {
@@ -47,7 +44,7 @@ func LoadPersona(path string) (*Persona, error) {
// 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 + ".yaml"
filename := name + ".json"
data, err := embeddedPersonas.ReadFile("personas/" + filename) // embed.FS paths use forward slashes per io/fs spec
if err != nil {
available := ListBuiltinPersonas()
@@ -57,10 +54,11 @@ func LoadBuiltinPersona(name string) (*Persona, error) {
}
// 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 nil
return []string{}
}
var names []string
for _, e := range entries {
@@ -68,10 +66,8 @@ func ListBuiltinPersonas() []string {
continue
}
name := e.Name()
if strings.HasSuffix(name, ".yaml") {
names = append(names, strings.TrimSuffix(name, ".yaml"))
} else if strings.HasSuffix(name, ".yml") {
names = append(names, strings.TrimSuffix(name, ".yml"))
if strings.HasSuffix(name, ".json") {
names = append(names, strings.TrimSuffix(name, ".json"))
}
}
return names
@@ -79,20 +75,9 @@ func ListBuiltinPersonas() []string {
func parsePersona(data []byte, source string) (*Persona, error) {
var p Persona
// Determine format by extension or try YAML first (it's a superset of JSON)
ext := strings.ToLower(filepath.Ext(source))
if ext == ".json" {
if err := json.Unmarshal(data, &p); err != nil {
return nil, fmt.Errorf("parse persona %s: %w", source, err)
}
} else {
// YAML (also handles .yaml, .yml, and builtin: prefix)
if err := yaml.Unmarshal(data, &p); err != nil {
return nil, fmt.Errorf("parse persona %s: %w", source, err)
}
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
}
@@ -112,3 +97,16 @@ func validatePersona(p *Persona, source string) error {
}
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:]
}