feat(recipe): add recipe system for Result[T] refactoring
Implement recipe loading, parsing, and running infrastructure. Add initial result-refactor recipe for converting Go error handling to monadic Result[T] style. Includes markdown recipe definition, YAML frontmatter parsing, step extraction, and basic runner.
This commit is contained in:
parent
736fe4fcd6
commit
aa38e92fb5
54
.grokkit/recipes/result-refactor.md
Normal file
54
.grokkit/recipes/result-refactor.md
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
name: result-refactor
|
||||
description: Convert traditional Go error handling to Result[T] monadic style
|
||||
version: 1.0
|
||||
|
||||
parameters:
|
||||
package_path:
|
||||
type: string
|
||||
default: internal/service
|
||||
description: Package to refactor
|
||||
dry_run:
|
||||
type: bool
|
||||
default: true
|
||||
description: If true, only generate patches
|
||||
|
||||
allowed_shell_commands:
|
||||
- go test ./...
|
||||
- go fmt ./...
|
||||
- go vet ./...
|
||||
- rg --files
|
||||
- git diff --name-only
|
||||
- jq
|
||||
|
||||
---
|
||||
|
||||
# Result[T] Refactoring Recipe
|
||||
|
||||
**Overview**
|
||||
Refactors all error handling in the target package to use the new Result[T] pattern.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Discover files
|
||||
**Objective:** Find every file that needs changing.
|
||||
**Instructions:** Recursively scan `{{.package_path}}` for `.go` files containing `if err != nil`.
|
||||
**Expected output:** A clean numbered list of full file paths (one per line).
|
||||
|
||||
### Step 2: Refactor each file
|
||||
**Objective:** Generate the updated code.
|
||||
**Instructions:** For each file from Step 1:
|
||||
- Read the full original content.
|
||||
- Refactor it to use `Result[T]` instead of naked errors (follow existing style, preserve comments).
|
||||
- Return *ONLY* the complete new file inside a ```go code block (no explanations).
|
||||
**Expected output:** One ```go block per file, clearly labelled with the filename.
|
||||
|
||||
### Step 3: Apply or patch
|
||||
**Objective:** Safely write changes or create reviewable output.
|
||||
**Instructions:**
|
||||
- If `dry_run` is true → create a unified diff patch file for review.
|
||||
- If false → write the new files (backup originals as `.bak`).
|
||||
**Expected output:** Confirmation of what was written + full path to any patch file.
|
||||
|
||||
### Final Summary
|
||||
Give me a concise executive summary: number of files changed, any warnings or patterns you noticed, and your recommended next step.
|
||||
82
internal/recipe/loader.go
Normal file
82
internal/recipe/loader.go
Normal file
@ -0,0 +1,82 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
stepRe = regexp.MustCompile(`(?m)^### Step (\d+): (.+)$`)
|
||||
subRe = regexp.MustCompile(`(?m)^(\*\*Objective:\*\*|\*\*Instructions:\*\*|\*\*Expected output:\*\*)\s*(.+?)(?=\n\n|\n###|\z)`)
|
||||
)
|
||||
|
||||
// Load reads a recipe from disk and fully parses it.
|
||||
func Load(path string, params map[string]any) (*Recipe, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// split frontmatter
|
||||
parts := bytes.SplitN(b, []byte("---"), 3)
|
||||
if len(parts) < 3 {
|
||||
return nil, fmt.Errorf("missing YAML frontmatter")
|
||||
}
|
||||
|
||||
var r Recipe
|
||||
if err := yaml.Unmarshal(parts[1], &r); err != nil {
|
||||
return nil, fmt.Errorf("yaml parse: %w", err)
|
||||
}
|
||||
|
||||
// simple template render on the whole body (so {{.package_path}} works everywhere)
|
||||
tpl, err := template.New("recipe").Parse(string(parts[2]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rendered bytes.Buffer
|
||||
if err := tpl.Execute(&rendered, params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body := rendered.String()
|
||||
|
||||
// extract steps
|
||||
matches := stepRe.FindAllStringSubmatch(body, -1)
|
||||
for i, m := range matches {
|
||||
stepNum := i + 1
|
||||
title := m[2]
|
||||
|
||||
// crude but effective sub-section extraction
|
||||
start := strings.Index(body, m[0])
|
||||
end := len(body)
|
||||
if i+1 < len(matches) {
|
||||
end = strings.Index(body[start:], matches[i+1][0]) + start
|
||||
}
|
||||
section := body[start:end]
|
||||
|
||||
step := Step{Number: stepNum, Title: title}
|
||||
for _, sub := range subRe.FindAllStringSubmatch(section, -1) {
|
||||
switch sub[1] {
|
||||
case "**Objective:**":
|
||||
step.Objective = strings.TrimSpace(sub[2])
|
||||
case "**Instructions:**":
|
||||
step.Instructions = strings.TrimSpace(sub[2])
|
||||
case "**Expected output:**":
|
||||
step.Expected = strings.TrimSpace(sub[2])
|
||||
}
|
||||
}
|
||||
r.Steps = append(r.Steps, step)
|
||||
}
|
||||
|
||||
// final summary is everything after the last step
|
||||
lastStepEnd := strings.LastIndex(body, matches[len(matches)-1][0])
|
||||
r.FinalSummaryPrompt = strings.TrimSpace(body[lastStepEnd+len(matches[len(matches)-1][0]):])
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
22
internal/recipe/runner.go
Normal file
22
internal/recipe/runner.go
Normal file
@ -0,0 +1,22 @@
|
||||
package recipe
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Runner struct {
|
||||
Recipe *Recipe
|
||||
}
|
||||
|
||||
func NewRunner(r *Recipe) *Runner {
|
||||
return &Runner{Recipe: r}
|
||||
}
|
||||
|
||||
func (r *Runner) Run() error {
|
||||
fmt.Printf("🍳 Starting recipe: %s v%s\n", r.Recipe.Name, r.Recipe.Version)
|
||||
for _, step := range r.Recipe.Steps {
|
||||
fmt.Printf("Step %d/%d: %s\n", step.Number, len(r.Recipe.Steps), step.Title)
|
||||
// TODO: here we will send step.Instructions to the LLM
|
||||
// and handle the response according to Expected
|
||||
}
|
||||
fmt.Println("✅ Recipe complete.")
|
||||
return nil
|
||||
}
|
||||
26
internal/recipe/types.go
Normal file
26
internal/recipe/types.go
Normal file
@ -0,0 +1,26 @@
|
||||
package recipe
|
||||
|
||||
type Recipe struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
Version string `yaml:"version"`
|
||||
Parameters map[string]Parameter `yaml:"parameters"`
|
||||
AllowedShellCommands []string `yaml:"allowed_shell_commands"`
|
||||
Overview string `yaml:"-"` // extracted from markdown
|
||||
Steps []Step `yaml:"-"`
|
||||
FinalSummaryPrompt string `yaml:"-"`
|
||||
}
|
||||
|
||||
type Parameter struct {
|
||||
Type string `yaml:"type"`
|
||||
Default any `yaml:"default"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
Number int
|
||||
Title string
|
||||
Objective string
|
||||
Instructions string
|
||||
Expected string
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user