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 }