refactor: Consolidate duplicated step collection functions in cook.go (bd-9btu)
Replaced three near-identical functions with a single unified collectSteps() that uses a callback strategy for label handling: - Removed collectStepsRecursive() (DB version) - Removed collectStepsToSubgraph() (in-memory version) - Removed collectStepIDMappings() (now handled by unified function) The new collectSteps() function: - Takes optional labelHandler callback for DB path (extracts labels separately) - Takes optional issueMap for in-memory path - Always builds idMapping for dependency resolution - Eliminates ~60 lines of duplicated code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
211
cmd/bd/cook.go
211
cmd/bd/cook.go
@@ -410,15 +410,13 @@ func cookFormulaToSubgraph(f *formula.Formula, protoID string) (*TemplateSubgrap
|
|||||||
issueMap[protoID] = rootIssue
|
issueMap[protoID] = rootIssue
|
||||||
|
|
||||||
// Collect issues for each step (use protoID as parent for step IDs)
|
// Collect issues for each step (use protoID as parent for step IDs)
|
||||||
collectStepsToSubgraph(f.Steps, protoID, issueMap, &issues, &deps)
|
// The unified collectSteps builds both issueMap and idMapping
|
||||||
|
idMapping := make(map[string]string)
|
||||||
|
collectSteps(f.Steps, protoID, idMapping, issueMap, &issues, &deps, nil) // nil = keep labels on issues
|
||||||
|
|
||||||
// Collect dependencies from depends_on
|
// Collect dependencies from depends_on using the idMapping built above
|
||||||
stepIDMapping := make(map[string]string)
|
|
||||||
for _, step := range f.Steps {
|
for _, step := range f.Steps {
|
||||||
collectStepIDMappings(step, protoID, stepIDMapping)
|
collectDependencies(step, idMapping, &deps)
|
||||||
}
|
|
||||||
for _, step := range f.Steps {
|
|
||||||
collectDependenciesToSubgraph(step, stepIDMapping, &deps)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &TemplateSubgraph{
|
return &TemplateSubgraph{
|
||||||
@@ -429,12 +427,10 @@ func cookFormulaToSubgraph(f *formula.Formula, protoID string) (*TemplateSubgrap
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectStepsToSubgraph collects issues and dependencies for steps and their children.
|
// processStepToIssue converts a formula.Step to a types.Issue.
|
||||||
// This is the in-memory version that doesn't create labels (since those require DB).
|
// The issue includes all fields including Labels populated from step.Labels and waits_for.
|
||||||
func collectStepsToSubgraph(steps []*formula.Step, parentID string, issueMap map[string]*types.Issue,
|
// This is the shared core logic used by both DB-persisted and in-memory cooking.
|
||||||
issues *[]*types.Issue, deps *[]*types.Dependency) {
|
func processStepToIssue(step *formula.Step, parentID string) *types.Issue {
|
||||||
|
|
||||||
for _, step := range steps {
|
|
||||||
// Generate issue ID (formula-name.step-id)
|
// Generate issue ID (formula-name.step-id)
|
||||||
issueID := fmt.Sprintf("%s.%s", parentID, step.ID)
|
issueID := fmt.Sprintf("%s.%s", parentID, step.ID)
|
||||||
|
|
||||||
@@ -465,7 +461,7 @@ func collectStepsToSubgraph(steps []*formula.Step, parentID string, issueMap map
|
|||||||
SourceLocation: step.SourceLocation, // Source tracing
|
SourceLocation: step.SourceLocation, // Source tracing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store labels in the issue's Labels field for in-memory use
|
// Populate labels from step
|
||||||
issue.Labels = append(issue.Labels, step.Labels...)
|
issue.Labels = append(issue.Labels, step.Labels...)
|
||||||
|
|
||||||
// Add gate label for waits_for field
|
// Add gate label for waits_for field
|
||||||
@@ -474,100 +470,56 @@ func collectStepsToSubgraph(steps []*formula.Step, parentID string, issueMap map
|
|||||||
issue.Labels = append(issue.Labels, gateLabel)
|
issue.Labels = append(issue.Labels, gateLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectSteps collects issues and dependencies for steps and their children.
|
||||||
|
// This is the unified implementation used by both DB-persisted and in-memory cooking.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - idMapping: step.ID → issue.ID (always populated, used for dependency resolution)
|
||||||
|
// - issueMap: issue.ID → issue (optional, nil for DB path, populated for in-memory path)
|
||||||
|
// - labelHandler: callback for each label (if nil, labels stay on issue; if set, labels are
|
||||||
|
// extracted and issue.Labels is cleared - use for DB path)
|
||||||
|
func collectSteps(steps []*formula.Step, parentID string,
|
||||||
|
idMapping map[string]string,
|
||||||
|
issueMap map[string]*types.Issue,
|
||||||
|
issues *[]*types.Issue,
|
||||||
|
deps *[]*types.Dependency,
|
||||||
|
labelHandler func(issueID, label string)) {
|
||||||
|
|
||||||
|
for _, step := range steps {
|
||||||
|
issue := processStepToIssue(step, parentID)
|
||||||
*issues = append(*issues, issue)
|
*issues = append(*issues, issue)
|
||||||
issueMap[issueID] = issue
|
|
||||||
|
// Build mappings
|
||||||
|
idMapping[step.ID] = issue.ID
|
||||||
|
if issueMap != nil {
|
||||||
|
issueMap[issue.ID] = issue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle labels: extract via callback (DB path) or keep on issue (in-memory path)
|
||||||
|
if labelHandler != nil {
|
||||||
|
for _, label := range issue.Labels {
|
||||||
|
labelHandler(issue.ID, label)
|
||||||
|
}
|
||||||
|
issue.Labels = nil // DB stores labels separately
|
||||||
|
}
|
||||||
|
|
||||||
// Add parent-child dependency
|
// Add parent-child dependency
|
||||||
*deps = append(*deps, &types.Dependency{
|
*deps = append(*deps, &types.Dependency{
|
||||||
IssueID: issueID,
|
IssueID: issue.ID,
|
||||||
DependsOnID: parentID,
|
DependsOnID: parentID,
|
||||||
Type: types.DepParentChild,
|
Type: types.DepParentChild,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Recursively collect children
|
// Recursively collect children
|
||||||
if len(step.Children) > 0 {
|
if len(step.Children) > 0 {
|
||||||
collectStepsToSubgraph(step.Children, issueID, issueMap, issues, deps)
|
collectSteps(step.Children, issue.ID, idMapping, issueMap, issues, deps, labelHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectStepIDMappings builds a map from step ID to full issue ID
|
|
||||||
func collectStepIDMappings(step *formula.Step, parentID string, mapping map[string]string) {
|
|
||||||
issueID := fmt.Sprintf("%s.%s", parentID, step.ID)
|
|
||||||
mapping[step.ID] = issueID
|
|
||||||
|
|
||||||
for _, child := range step.Children {
|
|
||||||
collectStepIDMappings(child, issueID, mapping)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectDependenciesToSubgraph collects blocking dependencies from depends_on and needs fields.
|
|
||||||
func collectDependenciesToSubgraph(step *formula.Step, idMapping map[string]string, deps *[]*types.Dependency) {
|
|
||||||
issueID := idMapping[step.ID]
|
|
||||||
|
|
||||||
// Process depends_on field
|
|
||||||
for _, depID := range step.DependsOn {
|
|
||||||
depIssueID, ok := idMapping[depID]
|
|
||||||
if !ok {
|
|
||||||
continue // Will be caught during validation
|
|
||||||
}
|
|
||||||
|
|
||||||
*deps = append(*deps, &types.Dependency{
|
|
||||||
IssueID: issueID,
|
|
||||||
DependsOnID: depIssueID,
|
|
||||||
Type: types.DepBlocks,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process needs field - simpler alias for sibling dependencies
|
|
||||||
for _, needID := range step.Needs {
|
|
||||||
needIssueID, ok := idMapping[needID]
|
|
||||||
if !ok {
|
|
||||||
continue // Will be caught during validation
|
|
||||||
}
|
|
||||||
|
|
||||||
*deps = append(*deps, &types.Dependency{
|
|
||||||
IssueID: issueID,
|
|
||||||
DependsOnID: needIssueID,
|
|
||||||
Type: types.DepBlocks,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process waits_for field - fanout gate dependency
|
|
||||||
if step.WaitsFor != "" {
|
|
||||||
waitsForSpec := formula.ParseWaitsFor(step.WaitsFor)
|
|
||||||
if waitsForSpec != nil {
|
|
||||||
// Determine spawner ID
|
|
||||||
spawnerStepID := waitsForSpec.SpawnerID
|
|
||||||
if spawnerStepID == "" && len(step.Needs) > 0 {
|
|
||||||
// Infer spawner from first need
|
|
||||||
spawnerStepID = step.Needs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if spawnerStepID != "" {
|
|
||||||
if spawnerIssueID, ok := idMapping[spawnerStepID]; ok {
|
|
||||||
// Create WaitsFor dependency with metadata
|
|
||||||
meta := types.WaitsForMeta{
|
|
||||||
Gate: waitsForSpec.Gate,
|
|
||||||
}
|
|
||||||
metaJSON, _ := json.Marshal(meta)
|
|
||||||
|
|
||||||
*deps = append(*deps, &types.Dependency{
|
|
||||||
IssueID: issueID,
|
|
||||||
DependsOnID: spawnerIssueID,
|
|
||||||
Type: types.DepWaitsFor,
|
|
||||||
Metadata: string(metaJSON),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively handle children
|
|
||||||
for _, child := range step.Children {
|
|
||||||
collectDependenciesToSubgraph(child, idMapping, deps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveAndCookFormula loads a formula by name, resolves it, applies all transformations,
|
// resolveAndCookFormula loads a formula by name, resolves it, applies all transformations,
|
||||||
// and returns an in-memory TemplateSubgraph ready for instantiation.
|
// and returns an in-memory TemplateSubgraph ready for instantiation.
|
||||||
@@ -694,7 +646,10 @@ func cookFormula(ctx context.Context, s storage.Storage, f *formula.Formula, pro
|
|||||||
labels = append(labels, struct{ issueID, label string }{protoID, MoleculeLabel})
|
labels = append(labels, struct{ issueID, label string }{protoID, MoleculeLabel})
|
||||||
|
|
||||||
// Collect issues for each step (use protoID as parent for step IDs)
|
// Collect issues for each step (use protoID as parent for step IDs)
|
||||||
collectStepsRecursive(f.Steps, protoID, idMapping, &issues, &deps, &labels)
|
// Use labelHandler to extract labels for separate DB storage
|
||||||
|
collectSteps(f.Steps, protoID, idMapping, nil, &issues, &deps, func(issueID, label string) {
|
||||||
|
labels = append(labels, struct{ issueID, label string }{issueID, label})
|
||||||
|
})
|
||||||
|
|
||||||
// Collect dependencies from depends_on
|
// Collect dependencies from depends_on
|
||||||
for _, step := range f.Steps {
|
for _, step := range f.Steps {
|
||||||
@@ -753,70 +708,8 @@ func cookFormula(ctx context.Context, s storage.Storage, f *formula.Formula, pro
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectStepsRecursive collects issues, dependencies, and labels for steps and their children.
|
// collectDependencies collects blocking dependencies from depends_on, needs, and waits_for fields.
|
||||||
func collectStepsRecursive(steps []*formula.Step, parentID string, idMapping map[string]string,
|
// This is the shared implementation used by both DB-persisted and in-memory subgraph cooking.
|
||||||
issues *[]*types.Issue, deps *[]*types.Dependency, labels *[]struct{ issueID, label string }) {
|
|
||||||
|
|
||||||
for _, step := range steps {
|
|
||||||
// Generate issue ID (formula-name.step-id)
|
|
||||||
issueID := fmt.Sprintf("%s.%s", parentID, step.ID)
|
|
||||||
|
|
||||||
// Determine issue type (children override to epic)
|
|
||||||
issueType := stepTypeToIssueType(step.Type)
|
|
||||||
if len(step.Children) > 0 {
|
|
||||||
issueType = types.TypeEpic
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine priority
|
|
||||||
priority := 2
|
|
||||||
if step.Priority != nil {
|
|
||||||
priority = *step.Priority
|
|
||||||
}
|
|
||||||
|
|
||||||
issue := &types.Issue{
|
|
||||||
ID: issueID,
|
|
||||||
Title: step.Title, // Keep {{variables}} for substitution at pour time
|
|
||||||
Description: step.Description,
|
|
||||||
Status: types.StatusOpen,
|
|
||||||
Priority: priority,
|
|
||||||
IssueType: issueType,
|
|
||||||
Assignee: step.Assignee,
|
|
||||||
IsTemplate: true,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
SourceFormula: step.SourceFormula, // Source tracing
|
|
||||||
SourceLocation: step.SourceLocation, // Source tracing
|
|
||||||
}
|
|
||||||
*issues = append(*issues, issue)
|
|
||||||
|
|
||||||
// Collect labels
|
|
||||||
for _, label := range step.Labels {
|
|
||||||
*labels = append(*labels, struct{ issueID, label string }{issueID, label})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add gate label for waits_for field
|
|
||||||
if step.WaitsFor != "" {
|
|
||||||
gateLabel := fmt.Sprintf("gate:%s", step.WaitsFor)
|
|
||||||
*labels = append(*labels, struct{ issueID, label string }{issueID, gateLabel})
|
|
||||||
}
|
|
||||||
|
|
||||||
idMapping[step.ID] = issueID
|
|
||||||
|
|
||||||
// Add parent-child dependency
|
|
||||||
*deps = append(*deps, &types.Dependency{
|
|
||||||
IssueID: issueID,
|
|
||||||
DependsOnID: parentID,
|
|
||||||
Type: types.DepParentChild,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Recursively collect children
|
|
||||||
if len(step.Children) > 0 {
|
|
||||||
collectStepsRecursive(step.Children, issueID, idMapping, issues, deps, labels)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectDependencies collects blocking dependencies from depends_on and needs fields.
|
|
||||||
func collectDependencies(step *formula.Step, idMapping map[string]string, deps *[]*types.Dependency) {
|
func collectDependencies(step *formula.Step, idMapping map[string]string, deps *[]*types.Dependency) {
|
||||||
issueID := idMapping[step.ID]
|
issueID := idMapping[step.ID]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user