feat: Add source tracing metadata to cooked steps (gt-8tmz.18)

Add SourceFormula and SourceLocation fields to track where each step
came from during the cooking process. This enables debugging of complex
compositions with inheritance, expansion, and advice.

Changes:
- Added SourceFormula and SourceLocation fields to Step struct (formula/types.go)
- Added same fields to Issue struct (types/types.go)
- Added SetSourceInfo() to parser.go - sets source on all steps after parsing
- Updated cook.go to copy source fields from Step to Issue
- Updated dry-run output to display source info: [from: formula@location]
- Updated advice.go to set source on advice-generated steps
- Updated controlflow.go to preserve source on loop-expanded steps
- Updated expand.go to preserve source on template-expanded steps

The source location format is:
- steps[N] - regular step at index N
- steps[N].children[M] - child step
- steps[N].loop.body[M] - loop body step
- template[N] - expansion template step
- advice - step inserted by advice transformation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-25 16:27:48 -08:00
parent b43358b600
commit bce4f8f2d4
8 changed files with 235 additions and 842 deletions

View File

@@ -166,10 +166,13 @@ func adviceStepToStep(as *AdviceStep, target *Step) *Step {
desc := substituteStepRef(as.Description, target)
return &Step{
ID: id,
Title: title,
Description: desc,
Type: as.Type,
ID: id,
Title: title,
Description: desc,
Type: as.Type,
SourceFormula: target.SourceFormula, // Inherit source formula from target (gt-8tmz.18)
// SourceLocation will be "advice" to indicate this came from advice transformation
SourceLocation: "advice",
}
}

View File

@@ -146,18 +146,20 @@ func expandLoopIteration(step *Step, iteration int) ([]*Step, error) {
iterID := fmt.Sprintf("%s.iter%d.%s", step.ID, iteration, bodyStep.ID)
clone := &Step{
ID: iterID,
Title: bodyStep.Title,
Description: bodyStep.Description,
Type: bodyStep.Type,
Priority: bodyStep.Priority,
Assignee: bodyStep.Assignee,
Condition: bodyStep.Condition,
WaitsFor: bodyStep.WaitsFor,
Expand: bodyStep.Expand,
Gate: bodyStep.Gate,
Loop: cloneLoopSpec(bodyStep.Loop), // Support nested loops (gt-zn35j)
OnComplete: cloneOnComplete(bodyStep.OnComplete),
ID: iterID,
Title: bodyStep.Title,
Description: bodyStep.Description,
Type: bodyStep.Type,
Priority: bodyStep.Priority,
Assignee: bodyStep.Assignee,
Condition: bodyStep.Condition,
WaitsFor: bodyStep.WaitsFor,
Expand: bodyStep.Expand,
Gate: bodyStep.Gate,
Loop: cloneLoopSpec(bodyStep.Loop), // Support nested loops (gt-zn35j)
OnComplete: cloneOnComplete(bodyStep.OnComplete),
SourceFormula: bodyStep.SourceFormula, // Preserve source (gt-8tmz.18)
SourceLocation: fmt.Sprintf("%s.iter%d", bodyStep.SourceLocation, iteration), // Track iteration
}
// Clone ExpandVars if present

View File

@@ -163,12 +163,14 @@ func expandStep(target *Step, template []*Step, depth int) ([]*Step, error) {
for _, tmpl := range template {
expanded := &Step{
ID: substituteTargetPlaceholders(tmpl.ID, target),
Title: substituteTargetPlaceholders(tmpl.Title, target),
Description: substituteTargetPlaceholders(tmpl.Description, target),
Type: tmpl.Type,
Priority: tmpl.Priority,
Assignee: tmpl.Assignee,
ID: substituteTargetPlaceholders(tmpl.ID, target),
Title: substituteTargetPlaceholders(tmpl.Title, target),
Description: substituteTargetPlaceholders(tmpl.Description, target),
Type: tmpl.Type,
Priority: tmpl.Priority,
Assignee: tmpl.Assignee,
SourceFormula: tmpl.SourceFormula, // Preserve source from template (gt-8tmz.18)
SourceLocation: tmpl.SourceLocation, // Preserve source location (gt-8tmz.18)
}
// Substitute placeholders in labels

View File

@@ -92,6 +92,10 @@ func (p *Parser) ParseFile(path string) (*Formula, error) {
}
formula.Source = absPath
// Set source tracing info on all steps (gt-8tmz.18)
SetSourceInfo(formula)
p.cache[absPath] = formula
// Also cache by name for extends resolution
@@ -393,3 +397,30 @@ func ApplyDefaults(formula *Formula, values map[string]string) map[string]string
return result
}
// SetSourceInfo sets SourceFormula and SourceLocation on all steps in a formula.
// Called after parsing to enable source tracing during cooking (gt-8tmz.18).
func SetSourceInfo(formula *Formula) {
setSourceInfoRecursive(formula.Steps, formula.Formula, "steps")
// Also set source info on template steps for expansion formulas
setSourceInfoRecursive(formula.Template, formula.Formula, "template")
}
// setSourceInfoRecursive recursively sets source info on steps.
func setSourceInfoRecursive(steps []*Step, formulaName, pathPrefix string) {
for i, step := range steps {
step.SourceFormula = formulaName
step.SourceLocation = fmt.Sprintf("%s[%d]", pathPrefix, i)
if len(step.Children) > 0 {
childPath := fmt.Sprintf("%s[%d].children", pathPrefix, i)
setSourceInfoRecursive(step.Children, formulaName, childPath)
}
// Handle loop body steps
if step.Loop != nil && len(step.Loop.Body) > 0 {
bodyPath := fmt.Sprintf("%s[%d].loop.body", pathPrefix, i)
setSourceInfoRecursive(step.Loop.Body, formulaName, bodyPath)
}
}
}

View File

@@ -188,6 +188,17 @@ type Step struct {
// OnComplete defines actions triggered when this step completes (gt-8tmz.8).
// Used for runtime expansion over step output (the for-each construct).
OnComplete *OnCompleteSpec `json:"on_complete,omitempty"`
// Source tracing fields (gt-8tmz.18): track where this step came from.
// These are set during parsing/transformation and copied to Issues during cooking.
// SourceFormula is the formula name where this step was defined.
// For inherited steps, this is the parent formula, not the final composed formula.
SourceFormula string `json:"-"` // Internal only, not serialized to JSON
// SourceLocation is the path within the source formula.
// Format: "steps[0]", "steps[2].children[1]", "advice[0].after", "loop.body[0]"
SourceLocation string `json:"-"` // Internal only, not serialized to JSON
}
// Gate defines an async wait condition (integrates with bd-udsi).

View File

@@ -66,6 +66,10 @@ type Issue struct {
AwaitID string `json:"await_id,omitempty"` // Condition identifier (e.g., run ID, PR number)
Timeout time.Duration `json:"timeout,omitempty"` // Max wait time before escalation
Waiters []string `json:"waiters,omitempty"` // Mail addresses to notify when gate clears
// Source tracing fields (gt-8tmz.18): track where this issue came from during cooking
SourceFormula string `json:"source_formula,omitempty"` // Formula name where this step was defined
SourceLocation string `json:"source_location,omitempty"` // Path within source: "steps[0]", "advice[0].after"
}
// ComputeContentHash creates a deterministic hash of the issue's content.