From e91f0de8d161fd1ed1e189cd090ad9213c746107 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Thu, 25 Dec 2025 20:46:12 -0800 Subject: [PATCH] feat(formula): Add inline step expansion (gt-8tmz.35) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps can now declare their own expansion using the Expand field: steps: - id: design expand: rule-of-five expand_vars: iterations: 3 This is more convenient than compose.expand for single-step expansions. The step is replaced by the expansion template with variables substituted. Reuses existing expandStep() and mergeVars() from gt-8tmz.34. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/cook.go | 16 ++++++++ internal/formula/expand.go | 79 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/cmd/bd/cook.go b/cmd/bd/cook.go index 95d0ef08..c2fa4652 100644 --- a/cmd/bd/cook.go +++ b/cmd/bd/cook.go @@ -157,6 +157,15 @@ func runCook(cmd *cobra.Command, args []string) { resolved.Steps = formula.ApplyAdvice(resolved.Steps, resolved.Advice) } + // Apply inline step expansions (gt-8tmz.35) + // This processes Step.Expand fields before compose.expand/map rules + inlineExpandedSteps, err := formula.ApplyInlineExpansions(resolved.Steps, parser) + if err != nil { + fmt.Fprintf(os.Stderr, "Error applying inline expansions: %v\n", err) + os.Exit(1) + } + resolved.Steps = inlineExpandedSteps + // Apply expansion operators (gt-8tmz.3) if resolved.Compose != nil && (len(resolved.Compose.Expand) > 0 || len(resolved.Compose.Map) > 0) { expandedSteps, err := formula.ApplyExpansions(resolved.Steps, resolved.Compose, parser) @@ -586,6 +595,13 @@ func resolveAndCookFormula(formulaName string, searchPaths []string) (*TemplateS resolved.Steps = formula.ApplyAdvice(resolved.Steps, resolved.Advice) } + // Apply inline step expansions (gt-8tmz.35) + inlineExpandedSteps, err := formula.ApplyInlineExpansions(resolved.Steps, parser) + if err != nil { + return nil, fmt.Errorf("applying inline expansions to %q: %w", formulaName, err) + } + resolved.Steps = inlineExpandedSteps + // Apply expansion operators (gt-8tmz.3) if resolved.Compose != nil && (len(resolved.Compose.Expand) > 0 || len(resolved.Compose.Map) > 0) { expandedSteps, err := formula.ApplyExpansions(resolved.Steps, resolved.Compose, parser) diff --git a/internal/formula/expand.go b/internal/formula/expand.go index b4738977..058f089c 100644 --- a/internal/formula/expand.go +++ b/internal/formula/expand.go @@ -360,3 +360,82 @@ func UpdateDependenciesForExpansion(steps []*Step, expandedID string, lastExpand return result } + +// ApplyInlineExpansions applies Step.Expand fields to inline expansions (gt-8tmz.35). +// Steps with the Expand field set are replaced by the referenced expansion template. +// The step's ExpandVars are passed as variable overrides to the expansion. +// +// This differs from compose.Expand in that the expansion is declared inline on the +// step itself rather than in a central compose section. +// +// Returns a new steps slice with inline expansions applied. +// The original steps slice is not modified. +func ApplyInlineExpansions(steps []*Step, parser *Parser) ([]*Step, error) { + if parser == nil { + return steps, nil + } + + return applyInlineExpansionsRecursive(steps, parser, 0) +} + +// applyInlineExpansionsRecursive handles inline expansions for a slice of steps. +// depth tracks recursion to prevent infinite expansion loops. +func applyInlineExpansionsRecursive(steps []*Step, parser *Parser, depth int) ([]*Step, error) { + if depth > DefaultMaxExpansionDepth { + return nil, fmt.Errorf("inline expansion depth limit exceeded: max %d levels", DefaultMaxExpansionDepth) + } + + var result []*Step + + for _, step := range steps { + // Check if this step has an inline expansion + if step.Expand != "" { + // Load the expansion formula + expFormula, err := parser.LoadByName(step.Expand) + if err != nil { + return nil, fmt.Errorf("inline expand on step %q: loading %q: %w", step.ID, step.Expand, err) + } + + if expFormula.Type != TypeExpansion { + return nil, fmt.Errorf("inline expand on step %q: %q is not an expansion formula (type=%s)", + step.ID, step.Expand, expFormula.Type) + } + + if len(expFormula.Template) == 0 { + return nil, fmt.Errorf("inline expand on step %q: %q has no template steps", step.ID, step.Expand) + } + + // Merge formula default vars with step's ExpandVars overrides + vars := mergeVars(expFormula, step.ExpandVars) + + // Expand the step using the template (reuse existing expandStep) + expandedSteps, err := expandStep(step, expFormula.Template, 0, vars) + if err != nil { + return nil, fmt.Errorf("inline expand on step %q: %w", step.ID, err) + } + + // Recursively process expanded steps for nested inline expansions + processedSteps, err := applyInlineExpansionsRecursive(expandedSteps, parser, depth+1) + if err != nil { + return nil, err + } + + result = append(result, processedSteps...) + } else { + // No inline expansion - keep the step, but process children recursively + clone := cloneStep(step) + + if len(step.Children) > 0 { + processedChildren, err := applyInlineExpansionsRecursive(step.Children, parser, depth) + if err != nil { + return nil, err + } + clone.Children = processedChildren + } + + result = append(result, clone) + } + } + + return result, nil +}