diff --git a/.grokkit/recipes/result-refactor.md b/.grokkit/recipes/result-refactor.md new file mode 100644 index 0000000..b89656b --- /dev/null +++ b/.grokkit/recipes/result-refactor.md @@ -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. \ No newline at end of file diff --git a/internal/recipe/loader.go b/internal/recipe/loader.go new file mode 100644 index 0000000..cc68f88 --- /dev/null +++ b/internal/recipe/loader.go @@ -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 +} diff --git a/internal/recipe/runner.go b/internal/recipe/runner.go new file mode 100644 index 0000000..f848619 --- /dev/null +++ b/internal/recipe/runner.go @@ -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 +} diff --git a/internal/recipe/types.go b/internal/recipe/types.go new file mode 100644 index 0000000..af00d9e --- /dev/null +++ b/internal/recipe/types.go @@ -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 +}