Add epic closure management commands (fixes #62)
- Add 'bd epic status' to show epic completion with child progress - Add 'bd epic close-eligible' to bulk-close completed epics - Add GetEpicsEligibleForClosure() storage method - Update 'bd stats' to show count of epics ready to close - Add EpicStatus type for tracking epic/child relationships - Support --eligible-only, --dry-run, and --json flags - Fix golangci-lint config version requirement Addresses GitHub issue #62 - epics now have visibility and management tools for closure when all children are complete. Amp-Thread-ID: https://ampcode.com/threads/T-e8ac3f48-f0cf-4858-8e8f-aace2481c30d Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
79
internal/storage/sqlite/epics.go
Normal file
79
internal/storage/sqlite/epics.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// GetEpicsEligibleForClosure returns all epics with their completion status
|
||||
func (s *SQLiteStorage) GetEpicsEligibleForClosure(ctx context.Context) ([]*types.EpicStatus, error) {
|
||||
query := `
|
||||
WITH epic_children AS (
|
||||
SELECT
|
||||
d.depends_on_id AS epic_id,
|
||||
i.id AS child_id,
|
||||
i.status AS child_status
|
||||
FROM dependencies d
|
||||
JOIN issues i ON i.id = d.issue_id
|
||||
WHERE d.type = 'parent-child'
|
||||
),
|
||||
epic_stats AS (
|
||||
SELECT
|
||||
epic_id,
|
||||
COUNT(*) AS total_children,
|
||||
SUM(CASE WHEN child_status = 'closed' THEN 1 ELSE 0 END) AS closed_children
|
||||
FROM epic_children
|
||||
GROUP BY epic_id
|
||||
)
|
||||
SELECT
|
||||
i.id, i.title, i.description, i.design, i.acceptance_criteria, i.notes,
|
||||
i.status, i.priority, i.issue_type, i.assignee, i.estimated_minutes,
|
||||
i.created_at, i.updated_at, i.closed_at, i.external_ref,
|
||||
COALESCE(es.total_children, 0) AS total_children,
|
||||
COALESCE(es.closed_children, 0) AS closed_children
|
||||
FROM issues i
|
||||
LEFT JOIN epic_stats es ON es.epic_id = i.id
|
||||
WHERE i.issue_type = 'epic'
|
||||
AND i.status != 'closed'
|
||||
ORDER BY i.priority ASC, i.created_at ASC
|
||||
`
|
||||
|
||||
rows, err := s.db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []*types.EpicStatus
|
||||
for rows.Next() {
|
||||
var epic types.Issue
|
||||
var totalChildren, closedChildren int
|
||||
|
||||
err := rows.Scan(
|
||||
&epic.ID, &epic.Title, &epic.Description, &epic.Design,
|
||||
&epic.AcceptanceCriteria, &epic.Notes, &epic.Status,
|
||||
&epic.Priority, &epic.IssueType, &epic.Assignee,
|
||||
&epic.EstimatedMinutes, &epic.CreatedAt, &epic.UpdatedAt,
|
||||
&epic.ClosedAt, &epic.ExternalRef,
|
||||
&totalChildren, &closedChildren,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eligibleForClose := false
|
||||
if totalChildren > 0 && closedChildren == totalChildren {
|
||||
eligibleForClose = true
|
||||
}
|
||||
|
||||
results = append(results, &types.EpicStatus{
|
||||
Epic: &epic,
|
||||
TotalChildren: totalChildren,
|
||||
ClosedChildren: closedChildren,
|
||||
EligibleForClose: eligibleForClose,
|
||||
})
|
||||
}
|
||||
|
||||
return results, rows.Err()
|
||||
}
|
||||
@@ -165,5 +165,35 @@ func (s *SQLiteStorage) GetStatistics(ctx context.Context) (*types.Statistics, e
|
||||
stats.AverageLeadTime = avgLeadTime.Float64
|
||||
}
|
||||
|
||||
// Get epics eligible for closure count
|
||||
err = s.db.QueryRowContext(ctx, `
|
||||
WITH epic_children AS (
|
||||
SELECT
|
||||
d.depends_on_id AS epic_id,
|
||||
i.status AS child_status
|
||||
FROM dependencies d
|
||||
JOIN issues i ON i.id = d.issue_id
|
||||
WHERE d.type = 'parent-child'
|
||||
),
|
||||
epic_stats AS (
|
||||
SELECT
|
||||
epic_id,
|
||||
COUNT(*) AS total_children,
|
||||
SUM(CASE WHEN child_status = 'closed' THEN 1 ELSE 0 END) AS closed_children
|
||||
FROM epic_children
|
||||
GROUP BY epic_id
|
||||
)
|
||||
SELECT COUNT(*)
|
||||
FROM issues i
|
||||
JOIN epic_stats es ON es.epic_id = i.id
|
||||
WHERE i.issue_type = 'epic'
|
||||
AND i.status != 'closed'
|
||||
AND es.total_children > 0
|
||||
AND es.closed_children = es.total_children
|
||||
`).Scan(&stats.EpicsEligibleForClosure)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get eligible epics count: %w", err)
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user