From 7a3498f8819d59f2afc306a668222b765895b1a9 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 28 Dec 2025 21:43:43 -0800 Subject: [PATCH] refactor: extract shared getEpicChildren helper for swarm commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add EpicChildren struct and getEpicChildren() helper function - Define SwarmStore interface for dependency injection - Refactor analyzeEpicForSwarm to use shared helper - Refactor getSwarmStatus to use shared helper - Eliminates duplicate code for fetching epic children and building dependency maps across both functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/swarm.go | 201 ++++++++++++++++++++++++------------------------ 1 file changed, 100 insertions(+), 101 deletions(-) diff --git a/cmd/bd/swarm.go b/cmd/bd/swarm.go index 913174fa..046ee757 100644 --- a/cmd/bd/swarm.go +++ b/cmd/bd/swarm.go @@ -58,6 +58,75 @@ type IssueNode struct { Wave int `json:"wave"` // Which ready front this belongs to (-1 if blocked by cycle) } +// EpicChildren holds the result of fetching an epic's children and their dependencies. +type EpicChildren struct { + Children []*types.Issue // Child issues of the epic + ChildIDSet map[string]bool // Set of child IDs for fast lookup + DependsOn map[string][]string // Map of issue ID -> IDs it depends on (within epic) +} + +// SwarmStore defines the interface needed for swarm operations. +type SwarmStore interface { + GetIssue(context.Context, string) (*types.Issue, error) + GetDependents(context.Context, string) ([]*types.Issue, error) + GetDependencyRecords(context.Context, string) ([]*types.Dependency, error) +} + +// getEpicChildren fetches all children of an epic and builds dependency maps. +// It filters to only parent-child relationships and only tracks blocking dependencies +// within the epic's children. +func getEpicChildren(ctx context.Context, s SwarmStore, epicID string) (*EpicChildren, error) { + result := &EpicChildren{ + Children: []*types.Issue{}, + ChildIDSet: make(map[string]bool), + DependsOn: make(map[string][]string), + } + + // Get all issues that depend on the epic + allDependents, err := s.GetDependents(ctx, epicID) + if err != nil { + return nil, fmt.Errorf("failed to get epic dependents: %w", err) + } + + // Filter to only parent-child relationships by checking each dependent's dependency records + for _, dependent := range allDependents { + deps, err := s.GetDependencyRecords(ctx, dependent.ID) + if err != nil { + continue // Skip issues we can't query + } + for _, dep := range deps { + if dep.DependsOnID == epicID && dep.Type == types.DepParentChild { + result.Children = append(result.Children, dependent) + result.ChildIDSet[dependent.ID] = true + break + } + } + } + + // Build dependency map (blocking dependencies within epic children only) + for _, issue := range result.Children { + deps, err := s.GetDependencyRecords(ctx, issue.ID) + if err != nil { + continue + } + for _, dep := range deps { + // Skip parent-child to epic itself + if dep.DependsOnID == epicID && dep.Type == types.DepParentChild { + continue + } + // Only track blocking dependencies within children + if !dep.Type.AffectsReadyWork() { + continue + } + if result.ChildIDSet[dep.DependsOnID] { + result.DependsOn[issue.ID] = append(result.DependsOn[issue.ID], dep.DependsOnID) + } + } + } + + return result, nil +} + var swarmValidateCmd = &cobra.Command{ Use: "validate [epic-id]", Short: "Validate epic structure for swarming", @@ -147,11 +216,7 @@ Examples: } // analyzeEpicForSwarm performs structural analysis of an epic for swarm execution. -func analyzeEpicForSwarm(ctx context.Context, s interface{ - GetIssue(context.Context, string) (*types.Issue, error) - GetDependents(context.Context, string) ([]*types.Issue, error) - GetDependencyRecords(context.Context, string) ([]*types.Dependency, error) -}, epic *types.Issue) (*SwarmAnalysis, error) { +func analyzeEpicForSwarm(ctx context.Context, s SwarmStore, epic *types.Issue) (*SwarmAnalysis, error) { analysis := &SwarmAnalysis{ EpicID: epic.ID, EpicTitle: epic.Title, @@ -159,45 +224,33 @@ func analyzeEpicForSwarm(ctx context.Context, s interface{ Issues: make(map[string]*IssueNode), } - // Get all issues that depend on the epic - allDependents, err := s.GetDependents(ctx, epic.ID) + // Get children and dependency map using shared helper + epicChildren, err := getEpicChildren(ctx, s, epic.ID) if err != nil { - return nil, fmt.Errorf("failed to get epic dependents: %w", err) + return nil, err } - // Filter to only parent-child relationships by checking each dependent's dependency records - var childIssues []*types.Issue - for _, dependent := range allDependents { - deps, err := s.GetDependencyRecords(ctx, dependent.ID) - if err != nil { - continue // Skip issues we can't query - } - for _, dep := range deps { - if dep.DependsOnID == epic.ID && dep.Type == types.DepParentChild { - childIssues = append(childIssues, dependent) - break - } - } - } - - if len(childIssues) == 0 { + if len(epicChildren.Children) == 0 { analysis.Warnings = append(analysis.Warnings, "Epic has no children") return analysis, nil } - analysis.TotalIssues = len(childIssues) + analysis.TotalIssues = len(epicChildren.Children) - // Build the issue graph - for _, issue := range childIssues { + // Build the issue graph with nodes + for _, issue := range epicChildren.Children { node := &IssueNode{ ID: issue.ID, Title: issue.Title, Status: string(issue.Status), Priority: issue.Priority, - DependsOn: []string{}, + DependsOn: epicChildren.DependsOn[issue.ID], // Use pre-computed deps DependedOnBy: []string{}, Wave: -1, // Will be set later } + if node.DependsOn == nil { + node.DependsOn = []string{} + } analysis.Issues[issue.ID] = node if issue.Status == types.StatusClosed { @@ -205,38 +258,30 @@ func analyzeEpicForSwarm(ctx context.Context, s interface{ } } - // Build dependency relationships (only within the epic's children) - childIDSet := make(map[string]bool) - for _, issue := range childIssues { - childIDSet[issue.ID] = true - } - - for _, issue := range childIssues { + // Build reverse dependency map (DependedOnBy) and check for external deps + for _, issue := range epicChildren.Children { deps, err := s.GetDependencyRecords(ctx, issue.ID) if err != nil { - return nil, fmt.Errorf("failed to get dependencies for %s: %w", issue.ID, err) + continue } - node := analysis.Issues[issue.ID] for _, dep := range deps { - // Only consider dependencies within the epic (not parent-child to epic itself) + // Skip parent-child to epic itself if dep.DependsOnID == epic.ID && dep.Type == types.DepParentChild { - continue // Skip the parent relationship to the epic + continue } // Only track blocking dependencies if !dep.Type.AffectsReadyWork() { continue } - // Only track dependencies within the epic's children - if childIDSet[dep.DependsOnID] { - node.DependsOn = append(node.DependsOn, dep.DependsOnID) + // Build DependedOnBy for internal deps + if epicChildren.ChildIDSet[dep.DependsOnID] { if targetNode, ok := analysis.Issues[dep.DependsOnID]; ok { targetNode.DependedOnBy = append(targetNode.DependedOnBy, issue.ID) } } - // External dependencies to issues outside the epic - if !childIDSet[dep.DependsOnID] && dep.DependsOnID != epic.ID { - // Check if it's an external ref + // Warn about external dependencies + if !epicChildren.ChildIDSet[dep.DependsOnID] && dep.DependsOnID != epic.ID { if strings.HasPrefix(dep.DependsOnID, "external:") { analysis.Warnings = append(analysis.Warnings, fmt.Sprintf("%s has external dependency: %s", issue.ID, dep.DependsOnID)) @@ -249,7 +294,7 @@ func analyzeEpicForSwarm(ctx context.Context, s interface{ } // Detect structural issues - detectStructuralIssues(analysis, childIssues) + detectStructuralIssues(analysis, epicChildren.Children) // Compute ready fronts (waves of parallel work) computeReadyFronts(analysis) @@ -613,10 +658,7 @@ Examples: } // getSwarmStatus computes current swarm status from beads. -func getSwarmStatus(ctx context.Context, s interface { - GetDependents(context.Context, string) ([]*types.Issue, error) - GetDependencyRecords(context.Context, string) ([]*types.Dependency, error) -}, epic *types.Issue) (*SwarmStatus, error) { +func getSwarmStatus(ctx context.Context, s SwarmStore, epic *types.Issue) (*SwarmStatus, error) { status := &SwarmStatus{ EpicID: epic.ID, EpicTitle: epic.Title, @@ -626,68 +668,25 @@ func getSwarmStatus(ctx context.Context, s interface { Blocked: []StatusIssue{}, } - // Get all issues that depend on the epic (children) - allDependents, err := s.GetDependents(ctx, epic.ID) + // Get children and dependency map using shared helper + epicChildren, err := getEpicChildren(ctx, s, epic.ID) if err != nil { - return nil, fmt.Errorf("failed to get epic dependents: %w", err) + return nil, err } - // Filter to only parent-child relationships - var childIssues []*types.Issue - for _, dependent := range allDependents { - deps, err := s.GetDependencyRecords(ctx, dependent.ID) - if err != nil { - continue - } - for _, dep := range deps { - if dep.DependsOnID == epic.ID && dep.Type == types.DepParentChild { - childIssues = append(childIssues, dependent) - break - } - } - } - - status.TotalIssues = len(childIssues) - if len(childIssues) == 0 { + status.TotalIssues = len(epicChildren.Children) + if len(epicChildren.Children) == 0 { return status, nil } - // Build set of child IDs for filtering - childIDSet := make(map[string]bool) - for _, issue := range childIssues { - childIDSet[issue.ID] = true - } - // Build status map for efficient blocked checks (avoids N+1 queries) statusMap := make(map[string]types.Status) - for _, issue := range childIssues { + for _, issue := range epicChildren.Children { statusMap[issue.ID] = issue.Status } - // Build dependency map (within epic children only) - dependsOn := make(map[string][]string) - for _, issue := range childIssues { - deps, err := s.GetDependencyRecords(ctx, issue.ID) - if err != nil { - continue - } - for _, dep := range deps { - // Skip parent-child to epic itself - if dep.DependsOnID == epic.ID && dep.Type == types.DepParentChild { - continue - } - // Only track blocking dependencies within children - if !dep.Type.AffectsReadyWork() { - continue - } - if childIDSet[dep.DependsOnID] { - dependsOn[issue.ID] = append(dependsOn[issue.ID], dep.DependsOnID) - } - } - } - // Categorize each issue - for _, issue := range childIssues { + for _, issue := range epicChildren.Children { si := StatusIssue{ ID: issue.ID, Title: issue.Title, @@ -706,7 +705,7 @@ func getSwarmStatus(ctx context.Context, s interface { default: // open or other // Check if blocked by open dependencies (uses statusMap, no extra queries) - deps := dependsOn[issue.ID] + deps := epicChildren.DependsOn[issue.ID] var blockers []string for _, depID := range deps { if depStatus, ok := statusMap[depID]; ok && depStatus != types.StatusClosed {