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