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:
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user