feat: add YAML support for persona files (#57)
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 9m33s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 9m55s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 10m32s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 11m0s
PR Ready Gate / clear-labels (pull_request) Successful in 1s
CI / test (pull_request) Successful in 9m33s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 9m55s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 10m32s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 11m0s
- Add gopkg.in/yaml.v3 dependency (approved in CONVENTIONS.md) - Update parsePersona to detect format by file extension - Support both .yaml and .yml extensions (case-insensitive) - Convert built-in personas to YAML format - Add comprehensive tests for YAML parsing - Update README with YAML examples and documentation YAML provides cleaner multi-line strings via literal block scalars and supports comments, making persona definitions more readable. JSON remains supported for backwards compatibility. Closes #57
This commit is contained in:
+60
-20
@@ -7,32 +7,35 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed personas/*.json
|
||||
//go:embed personas/*.yaml
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Major string `json:"major" yaml:"major"`
|
||||
Minor string `json:"minor" yaml:"minor"`
|
||||
Nit string `json:"nit" yaml:"nit"`
|
||||
}
|
||||
|
||||
// LoadPersona loads a persona from a JSON file path.
|
||||
// LoadPersona loads a persona from a JSON or YAML file path.
|
||||
// Format is detected by file extension: .yaml/.yml for YAML, .json or other for JSON.
|
||||
func LoadPersona(path string) (*Persona, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -43,14 +46,23 @@ func LoadPersona(path string) (*Persona, error) {
|
||||
|
||||
// LoadBuiltinPersona loads a built-in persona by name.
|
||||
// Returns an error if the persona doesn't exist.
|
||||
// Built-in personas are stored in YAML format.
|
||||
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
|
||||
// Try YAML first (preferred format)
|
||||
yamlFile := name + ".yaml"
|
||||
data, err := embeddedPersonas.ReadFile("personas/" + yamlFile)
|
||||
if err == nil {
|
||||
return parsePersona(data, "builtin:"+yamlFile)
|
||||
}
|
||||
|
||||
// Fall back to JSON for backwards compatibility
|
||||
jsonFile := name + ".json"
|
||||
data, err = embeddedPersonas.ReadFile("personas/" + jsonFile)
|
||||
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)
|
||||
return parsePersona(data, "builtin:"+jsonFile)
|
||||
}
|
||||
|
||||
// ListBuiltinPersonas returns the names of all built-in personas.
|
||||
@@ -60,22 +72,50 @@ func ListBuiltinPersonas() []string {
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
var names []string
|
||||
seen := make(map[string]bool)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if strings.HasSuffix(name, ".json") {
|
||||
names = append(names, strings.TrimSuffix(name, ".json"))
|
||||
// Strip extension to get persona name
|
||||
var personaName string
|
||||
switch {
|
||||
case strings.HasSuffix(name, ".yaml"):
|
||||
personaName = strings.TrimSuffix(name, ".yaml")
|
||||
case strings.HasSuffix(name, ".yml"):
|
||||
personaName = strings.TrimSuffix(name, ".yml")
|
||||
case strings.HasSuffix(name, ".json"):
|
||||
personaName = strings.TrimSuffix(name, ".json")
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if !seen[personaName] {
|
||||
seen[personaName] = true
|
||||
}
|
||||
}
|
||||
var names []string
|
||||
for name := range seen {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// parsePersona parses persona data from JSON or YAML format.
|
||||
// Format is detected by the source file extension.
|
||||
func parsePersona(data []byte, source string) (*Persona, error) {
|
||||
lowerSource := strings.ToLower(source)
|
||||
isYAML := strings.HasSuffix(lowerSource, ".yaml") || strings.HasSuffix(lowerSource, ".yml")
|
||||
|
||||
var p Persona
|
||||
if err := json.Unmarshal(data, &p); err != nil {
|
||||
var err error
|
||||
if isYAML {
|
||||
// go-yaml v1.16.0+ has built-in protection against deeply nested structures
|
||||
err = yaml.Unmarshal(data, &p)
|
||||
} else {
|
||||
err = json.Unmarshal(data, &p)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse persona %s: %w", source, err)
|
||||
}
|
||||
if err := validatePersona(&p, source); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user