feat: add bd mol progress command for efficient molecule monitoring (bd-8xnf)

Adds a new `bd mol progress` command that shows molecule progress using
indexed queries instead of loading all steps into memory. This makes it
suitable for mega-molecules with millions of steps.

Features:
- Efficient SQL-based counting via idx_dependencies_depends_on_type index
- Progress display: completed / total (percentage)
- Current step identification
- Rate calculation from closure timestamps
- ETA estimation
- JSON output support

New storage interface method: GetMoleculeProgress(ctx, moleculeID)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beads/crew/fang
2025-12-31 12:35:41 -08:00
committed by Steve Yegge
parent 3593ba8d66
commit 0a96b10bba
7 changed files with 1210 additions and 870 deletions

View File

@@ -207,3 +207,74 @@ func (s *SQLiteStorage) GetStatistics(ctx context.Context) (*types.Statistics, e
return &stats, nil
}
// GetMoleculeProgress returns efficient progress stats for a molecule.
// Uses indexed queries on dependencies table instead of loading all steps.
func (s *SQLiteStorage) GetMoleculeProgress(ctx context.Context, moleculeID string) (*types.MoleculeProgressStats, error) {
// First get the molecule's title
var title string
err := s.db.QueryRowContext(ctx, `SELECT title FROM issues WHERE id = ?`, moleculeID).Scan(&title)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("molecule not found: %s", moleculeID)
}
return nil, fmt.Errorf("failed to get molecule: %w", err)
}
stats := &types.MoleculeProgressStats{
MoleculeID: moleculeID,
MoleculeTitle: title,
}
// Get counts from direct children via parent-child dependency
// Uses idx_dependencies_depends_on_type index
err = s.db.QueryRowContext(ctx, `
SELECT
COUNT(*) as total,
COALESCE(SUM(CASE WHEN i.status = 'closed' THEN 1 ELSE 0 END), 0) as completed,
COALESCE(SUM(CASE WHEN i.status = 'in_progress' THEN 1 ELSE 0 END), 0) as in_progress
FROM dependencies d
JOIN issues i ON d.issue_id = i.id
WHERE d.depends_on_id = ? AND d.type = 'parent-child'
`, moleculeID).Scan(&stats.Total, &stats.Completed, &stats.InProgress)
if err != nil {
return nil, fmt.Errorf("failed to get child counts: %w", err)
}
// Get first in_progress step ID (for "current step" display)
var currentStepID sql.NullString
err = s.db.QueryRowContext(ctx, `
SELECT i.id
FROM dependencies d
JOIN issues i ON d.issue_id = i.id
WHERE d.depends_on_id = ? AND d.type = 'parent-child' AND i.status = 'in_progress'
ORDER BY i.created_at
LIMIT 1
`, moleculeID).Scan(&currentStepID)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("failed to get current step: %w", err)
}
if currentStepID.Valid {
stats.CurrentStepID = currentStepID.String
}
// Get first and last closure times for rate calculation
var firstClosed, lastClosed sql.NullTime
err = s.db.QueryRowContext(ctx, `
SELECT MIN(i.closed_at), MAX(i.closed_at)
FROM dependencies d
JOIN issues i ON d.issue_id = i.id
WHERE d.depends_on_id = ? AND d.type = 'parent-child' AND i.status = 'closed'
`, moleculeID).Scan(&firstClosed, &lastClosed)
if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("failed to get closure times: %w", err)
}
if firstClosed.Valid {
stats.FirstClosed = &firstClosed.Time
}
if lastClosed.Valid {
stats.LastClosed = &lastClosed.Time
}
return stats, nil
}