From 7cb9eb3eb7f9306dae641daef18ef68643de952d Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Fri, 6 Mar 2026 22:40:29 +0000 Subject: [PATCH] refactor(recipe): switch refactor step to strict JSON output - Update result-refactor.md to output JSON array of file changes instead of code blocks - Modify runner.go to parse JSON directly, removing regex-based extraction - Add project_languages and extensions to recipe metadata - Improve error handling and output consistency in steps --- .grokkit/recipes/result-refactor.md | 34 ++++++++++++------ internal/recipe/runner.go | 56 ++++++++++++++++------------- 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/.grokkit/recipes/result-refactor.md b/.grokkit/recipes/result-refactor.md index c83cd59..cc9ab3f 100644 --- a/.grokkit/recipes/result-refactor.md +++ b/.grokkit/recipes/result-refactor.md @@ -13,6 +13,13 @@ parameters: default: true description: If true, only generate patches +project_languages: + - go + +extensions: + go: + - .go + allowed_shell_commands: - go test ./... - go fmt ./... @@ -33,23 +40,30 @@ Refactors all error handling in the target package to use the new Result[T] patt ### Step 1: Discover files **Objective:** Find every file that needs changing. **Instructions:** Recursively scan `{{.package_path}}` for `.go` files containing `if err != nil`. -If no files are found or the path does not exist, output exactly: "No files found matching the criteria." -**Expected output:** A clean numbered list of full file paths (one per line). +**Expected output:** A clean list of full file paths (one per line). If none, say "No files found matching the criteria." ### 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. +- Refactor it to use `Result[T]` instead of naked errors (follow existing style, preserve all comments). +- Return **ONLY** a single JSON array in this exact format (no extra text, no markdown): +```json +[ + { + "file": "internal/example.go", + "content": "the complete refactored file here" + } +] +``` ### Step 3: Apply or patch -**Objective:** Safely write changes or create reviewable output. +**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. +- 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 +**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/runner.go b/internal/recipe/runner.go index 649eadc..4899a83 100644 --- a/internal/recipe/runner.go +++ b/internal/recipe/runner.go @@ -2,10 +2,10 @@ package recipe import ( _ "bufio" + "encoding/json" "fmt" "os" "path/filepath" - "regexp" "strings" "gmgauthier.com/grokkit/internal/grok" @@ -99,7 +99,7 @@ func (r *Runner) discoverFiles() []string { return files } -// handleApplyStep with robust regex that matches Grok's current output +// handleApplyStep parses strict JSON from the refactor step — no regex at all func (r *Runner) handleApplyStep(previousResults []string) { if len(previousResults) == 0 { fmt.Println(" ⚠️ No previous results to apply — skipping.") @@ -107,16 +107,36 @@ func (r *Runner) handleApplyStep(previousResults []string) { } lastResult := previousResults[len(previousResults)-1] - blocks := extractCodeBlocks(lastResult) - if len(blocks) == 0 { - fmt.Println(" ⚠️ No code blocks found to apply — skipping.") + // Find the JSON array in the response + start := strings.Index(lastResult, "[") + end := strings.LastIndex(lastResult, "]") + 1 + if start == -1 || end == 0 { + fmt.Println(" ⚠️ No JSON found in previous step — skipping.") + return + } + + jsonStr := lastResult[start:end] + + type FileChange struct { + File string `json:"file"` + Content string `json:"content"` + } + + var changes []FileChange + if err := json.Unmarshal([]byte(jsonStr), &changes); err != nil { + fmt.Printf(" ⚠️ Could not parse JSON: %v\n", err) + return + } + + if len(changes) == 0 { + fmt.Println(" ⚠️ No files to apply — skipping.") return } fmt.Println(" 📄 Dry-run mode: creating patch file...") patchPath := filepath.Join(".", "recipe-refactor.patch") - if err := createUnifiedPatch(blocks, patchPath); err != nil { + if err := createUnifiedPatch(changes, patchPath); err != nil { fmt.Printf(" ❌ Failed to create patch: %v\n", err) return } @@ -124,32 +144,18 @@ func (r *Runner) handleApplyStep(previousResults []string) { fmt.Println(" Review it, then run with dry_run=false to apply.") } -// Double-quoted regex — no raw-string backtick problems ever again -var blockRe = regexp.MustCompile(`(?s)```go\n// \s*(.+?\.go)\n(.*?)\n````) - -func extractCodeBlocks(text string) map[string]string { - blocks := make(map[string]string) - matches := blockRe.FindAllStringSubmatch(text, -1) - for _, m := range matches { - if len(m) == 3 { - blocks[m[1]] = m[2] - } - } - return blocks -} - -func createUnifiedPatch(blocks map[string]string, patchPath string) error { +func createUnifiedPatch(changes []struct{ File, Content string }, patchPath string) error { f, err := os.Create(patchPath) if err != nil { return err } defer f.Close() - for path, content := range blocks { - fmt.Fprintf(f, "--- %s\n+++ %s\n@@ -0,0 +1,%d @@\n", path, path, strings.Count(content, "\n")+1) - for _, line := range strings.Split(content, "\n") { + for _, ch := range changes { + fmt.Fprintf(f, "--- %s\n+++ %s\n@@ -0,0 +1,%d @@\n", ch.File, ch.File, strings.Count(ch.Content, "\n")+1) + for _, line := range strings.Split(ch.Content, "\n") { fmt.Fprintf(f, "+%s\n", line) } } return nil -} \ No newline at end of file +}