Files
beads/internal/formula/advice.go
Steve Yegge a643b9ab67 Implement advice operators for formula composition (gt-8tmz.2)
Add Lisp-style advice operators to the formula DSL:
- before(target, step) - insert step before target
- after(target, step) - insert step after target
- around(target, wrapper) - wrap target with before/after

Features:
- Glob pattern matching for targets (*.implement, shiny.*, etc)
- Pointcut matching by type or label
- Step reference substitution ({step.id}, {step.title})
- Automatic dependency chaining

New types: AdviceRule, AdviceStep, AroundAdvice, Pointcut
New functions: ApplyAdvice, MatchGlob, MatchPointcut

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 01:53:43 -08:00

235 lines
5.9 KiB
Go

// Package formula provides advice operators for step transformations.
//
// Advice operators are Lisp-style transformations that insert steps
// before, after, or around matching target steps. They enable
// cross-cutting concerns like logging, security scanning, or
// approval gates to be applied declaratively.
//
// Supported patterns:
// - "design" - exact match
// - "*.implement" - suffix match (any step ending in .implement)
// - "shiny.*" - prefix match (any step starting with shiny.)
// - "*" - match all steps
package formula
import (
"path/filepath"
"strings"
)
// MatchGlob checks if a step ID matches a glob pattern.
// Supported patterns:
// - "exact" - exact match
// - "*.suffix" - ends with .suffix
// - "prefix.*" - starts with prefix.
// - "*" - matches everything
// - "prefix.*.suffix" - starts with prefix. and ends with .suffix
func MatchGlob(pattern, stepID string) bool {
// Use filepath.Match for basic glob support
matched, err := filepath.Match(pattern, stepID)
if err == nil && matched {
return true
}
// Handle additional patterns
if pattern == "*" {
return true
}
// *.suffix pattern (e.g., "*.implement")
if strings.HasPrefix(pattern, "*.") {
suffix := pattern[1:] // ".implement"
return strings.HasSuffix(stepID, suffix)
}
// prefix.* pattern (e.g., "shiny.*")
if strings.HasSuffix(pattern, ".*") {
prefix := pattern[:len(pattern)-1] // "shiny."
return strings.HasPrefix(stepID, prefix)
}
// Exact match
return pattern == stepID
}
// ApplyAdvice transforms a formula's steps by applying advice rules.
// Returns a new steps slice with advice steps inserted.
// The original steps slice is not modified.
func ApplyAdvice(steps []*Step, advice []*AdviceRule) []*Step {
if len(advice) == 0 {
return steps
}
result := make([]*Step, 0, len(steps)*2) // Pre-allocate for insertions
for _, step := range steps {
// Find matching advice rules for this step
var beforeSteps []*Step
var afterSteps []*Step
for _, rule := range advice {
if !MatchGlob(rule.Target, step.ID) {
continue
}
// Collect before steps
if rule.Before != nil {
beforeSteps = append(beforeSteps, adviceStepToStep(rule.Before, step))
}
if rule.Around != nil {
for _, as := range rule.Around.Before {
beforeSteps = append(beforeSteps, adviceStepToStep(as, step))
}
}
// Collect after steps
if rule.After != nil {
afterSteps = append(afterSteps, adviceStepToStep(rule.After, step))
}
if rule.Around != nil {
for _, as := range rule.Around.After {
afterSteps = append(afterSteps, adviceStepToStep(as, step))
}
}
}
// Insert before steps
for _, bs := range beforeSteps {
result = append(result, bs)
}
// Clone the original step and update its dependencies
clonedStep := cloneStep(step)
// If there are before steps, the original step needs to depend on the last before step
if len(beforeSteps) > 0 {
lastBefore := beforeSteps[len(beforeSteps)-1]
clonedStep.Needs = appendUnique(clonedStep.Needs, lastBefore.ID)
}
// Chain before steps together
for i := 1; i < len(beforeSteps); i++ {
beforeSteps[i].Needs = appendUnique(beforeSteps[i].Needs, beforeSteps[i-1].ID)
}
result = append(result, clonedStep)
// Insert after steps and chain them
for i, as := range afterSteps {
if i == 0 {
// First after step depends on the original step
as.Needs = appendUnique(as.Needs, step.ID)
} else {
// Subsequent after steps chain to previous
as.Needs = appendUnique(as.Needs, afterSteps[i-1].ID)
}
result = append(result, as)
}
// Recursively apply advice to children
if len(step.Children) > 0 {
clonedStep.Children = ApplyAdvice(step.Children, advice)
}
}
return result
}
// adviceStepToStep converts an AdviceStep to a Step.
// Substitutes {step.id} placeholders with the target step's ID.
func adviceStepToStep(as *AdviceStep, target *Step) *Step {
// Substitute {step.id} in ID and Title
id := substituteStepRef(as.ID, target)
title := substituteStepRef(as.Title, target)
if title == "" {
title = id
}
desc := substituteStepRef(as.Description, target)
return &Step{
ID: id,
Title: title,
Description: desc,
Type: as.Type,
}
}
// substituteStepRef replaces {step.id} with the target step's ID.
func substituteStepRef(s string, target *Step) string {
s = strings.ReplaceAll(s, "{step.id}", target.ID)
s = strings.ReplaceAll(s, "{step.title}", target.Title)
return s
}
// cloneStep creates a shallow copy of a step.
func cloneStep(s *Step) *Step {
clone := *s
// Deep copy slices
if len(s.DependsOn) > 0 {
clone.DependsOn = make([]string, len(s.DependsOn))
copy(clone.DependsOn, s.DependsOn)
}
if len(s.Needs) > 0 {
clone.Needs = make([]string, len(s.Needs))
copy(clone.Needs, s.Needs)
}
if len(s.Labels) > 0 {
clone.Labels = make([]string, len(s.Labels))
copy(clone.Labels, s.Labels)
}
// Don't deep copy children here - ApplyAdvice handles that recursively
return &clone
}
// appendUnique appends an item to a slice if not already present.
func appendUnique(slice []string, item string) []string {
for _, s := range slice {
if s == item {
return slice
}
}
return append(slice, item)
}
// MatchPointcut checks if a step matches a pointcut.
func MatchPointcut(pc *Pointcut, step *Step) bool {
// Glob match on step ID
if pc.Glob != "" && !MatchGlob(pc.Glob, step.ID) {
return false
}
// Type match
if pc.Type != "" && step.Type != pc.Type {
return false
}
// Label match
if pc.Label != "" {
found := false
for _, l := range step.Labels {
if l == pc.Label {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// MatchAnyPointcut checks if a step matches any pointcut in the list.
func MatchAnyPointcut(pointcuts []*Pointcut, step *Step) bool {
if len(pointcuts) == 0 {
return true // No pointcuts means match all
}
for _, pc := range pointcuts {
if MatchPointcut(pc, step) {
return true
}
}
return false
}