From e2c42c35c498f481d6b60217da7c178a45ab0a60 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Wed, 15 Oct 2025 23:51:57 -0700 Subject: [PATCH] Add label and title filtering to bd list (closes #45, bd-269) - Add --label/-l flag to filter issues by labels (AND logic) - Add --title flag to filter issues by title substring - Add TitleSearch field to IssueFilter type - Implement label and title filtering in SearchIssues - Perfect for worktree-specific issue management Examples: bd list --label worktree,feature-x bd list --title "authentication" bd list --label worktree --title "bug" --- .beads/issues.jsonl | 1 + cmd/bd/list.go | 10 ++++++++++ internal/storage/sqlite/sqlite.go | 14 ++++++++++++++ internal/types/types.go | 13 +++++++------ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 602e3f3e..d4ac6229 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -185,6 +185,7 @@ {"id":"bd-266","title":"Optional: Implement auto-compaction","description":"Implement automatic compaction triggered by certain operations when enabled via config.","design":"Trigger points (when `auto_compact_enabled = true`):\n1. `bd stats` - check and compact if candidates exist\n2. `bd export` - before exporting\n3. Configurable: on any read operation after N candidates accumulate\n\nAdd:\n```go\nfunc (s *SQLiteStorage) AutoCompact(ctx context.Context) error {\n enabled, _ := s.GetConfig(ctx, \"auto_compact_enabled\")\n if enabled != \"true\" {\n return nil\n }\n\n // Run Tier 1 compaction on all candidates\n // Limit to batch_size to avoid long operations\n // Log activity for transparency\n}\n```","acceptance_criteria":"- Respects auto_compact_enabled config (default: false)\n- Limits batch size to avoid blocking operations\n- Logs compaction activity (visible with --verbose)\n- Can be disabled per-command with `--no-auto-compact` flag\n- Only compacts Tier 1 (Tier 2 remains manual)\n- Doesn't run more than once per hour (rate limiting)","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-15T21:51:23.281006-07:00","updated_at":"2025-10-15T21:51:23.281006-07:00"} {"id":"bd-267","title":"Optional: Add git commit counting","description":"Implement git commit counting for \"project time\" measurement as alternative to calendar time for Tier 2 eligibility.","design":"```go\nfunc getCommitsSince(closedAt time.Time) (int, error) {\n cmd := exec.Command(\"git\", \"rev-list\", \"--count\",\n fmt.Sprintf(\"--since=%s\", closedAt.Format(time.RFC3339)), \"HEAD\")\n output, err := cmd.Output()\n if err != nil {\n return 0, err // Not in git repo or git not available\n }\n return strconv.Atoi(strings.TrimSpace(string(output)))\n}\n```\n\nFallback strategies:\n1. Git commit count (preferred)\n2. Issue counter delta (store counter at close time, compare later)\n3. Pure time-based (90 days)","acceptance_criteria":"- Counts commits since closed_at timestamp\n- Handles git not available gracefully (falls back)\n- Fallback to issue counter delta works\n- Configurable via compact_tier2_commits config key\n- Tested with real git repo\n- Works in non-git environments","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-15T21:51:23.284781-07:00","updated_at":"2025-10-15T21:51:23.284781-07:00"} {"id":"bd-268","title":"Explore in-memory embedded SQL alternatives to SQLite","description":"Investigate lightweight in-memory embedded SQL databases as alternative backends for environments where SQLite is problematic or considered too heavyweight. This would provide flexibility for different deployment scenarios.","design":"Research options:\n- modernc.org/sqlite (pure Go SQLite implementation, no cgo)\n- rqlite (distributed SQLite with Raft)\n- go-memdb (in-memory database by HashiCorp)\n- badger (embedded key-value store, would need SQL layer)\n- bbolt (embedded key-value store)\n- duckdb (lightweight analytical database)\n\nEvaluate on:\n- Memory footprint vs SQLite\n- cgo dependency (pure Go preferred)\n- SQL compatibility level\n- Transaction support\n- Performance characteristics\n- Maintenance/community status\n- Migration complexity from SQLite\n\nConsider creating a storage abstraction layer to support multiple backends.","acceptance_criteria":"- Document comparison of at least 3 alternatives\n- Benchmark memory usage and performance vs SQLite\n- Assess migration effort for each option\n- Recommendation on whether to support alternatives\n- If yes, prototype storage interface abstraction","notes":"Worth noting: modernc.org/sqlite is a pure Go implementation (no cgo) that might already address the \"heavyweight\" concern, since much of SQLite's overhead comes from cgo calls. Should evaluate this first before exploring completely different database technologies.","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-15T23:17:33.560045-07:00","updated_at":"2025-10-15T23:18:15.120205-07:00"} +{"id":"bd-269","title":"Add label and title filtering to bd list","description":"Add --label and --title flags to bd list command for better filtering. Labels backend already exists, just need CLI exposure. Title search needs both backend and CLI implementation.","status":"in_progress","priority":1,"issue_type":"feature","created_at":"2025-10-15T23:49:08.199238-07:00","updated_at":"2025-10-15T23:51:44.050122-07:00","external_ref":"gh-45"} {"id":"bd-27","title":"Cache compiled regexes in replaceIDReferences for performance","description":"replaceIDReferences() compiles the same regex patterns on every call. With 100 issues and 10 ID mappings, that's 1000 regex compilations. Pre-compile regexes once and reuse. Can use a struct with compiled regex, placeholder, and newID. Located in collision.go:329. Estimated performance improvement: 10-100x for large batches.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-10-14T14:43:06.911892-07:00","updated_at":"2025-10-15T16:27:22.002496-07:00","closed_at":"2025-10-15T03:01:29.570955-07:00"} {"id":"bd-28","title":"Improve error handling in dependency removal during remapping","description":"In updateDependencyReferences(), RemoveDependency errors are caught and ignored with continue (line 392). Comment says 'if dependency doesn't exist' but this catches ALL errors including real failures. Should check error type with errors.Is(err, ErrDependencyNotFound) and only ignore not-found errors, returning other errors properly.","status":"open","priority":3,"issue_type":"bug","created_at":"2025-10-14T14:43:06.912228-07:00","updated_at":"2025-10-15T16:27:22.003145-07:00"} {"id":"bd-29","title":"Use safer placeholder pattern in replaceIDReferences","description":"Currently uses __PLACEHOLDER_0__ which could theoretically collide with user text. Use a truly unique placeholder like null bytes: \\x00REMAP\\x00_0_\\x00 which are unlikely to appear in normal text. Located in collision.go:324. Very low probability issue but worth fixing for completeness.","status":"open","priority":3,"issue_type":"task","created_at":"2025-10-14T14:43:06.912567-07:00","updated_at":"2025-10-15T16:27:22.003668-07:00"} diff --git a/cmd/bd/list.go b/cmd/bd/list.go index e76b8f0b..2802ee79 100644 --- a/cmd/bd/list.go +++ b/cmd/bd/list.go @@ -21,6 +21,8 @@ var listCmd = &cobra.Command{ issueType, _ := cmd.Flags().GetString("type") limit, _ := cmd.Flags().GetInt("limit") formatStr, _ := cmd.Flags().GetString("format") + labels, _ := cmd.Flags().GetStringSlice("label") + titleSearch, _ := cmd.Flags().GetString("title") filter := types.IssueFilter{ Limit: limit, @@ -41,6 +43,12 @@ var listCmd = &cobra.Command{ t := types.IssueType(issueType) filter.IssueType = &t } + if len(labels) > 0 { + filter.Labels = labels + } + if titleSearch != "" { + filter.TitleSearch = titleSearch + } ctx := context.Background() issues, err := store.SearchIssues(ctx, "", filter) @@ -80,6 +88,8 @@ func init() { listCmd.Flags().IntP("priority", "p", 0, "Filter by priority (0-4: 0=critical, 1=high, 2=medium, 3=low, 4=backlog)") listCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") listCmd.Flags().StringP("type", "t", "", "Filter by type (bug, feature, task, epic, chore)") + listCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (comma-separated, must have ALL labels)") + listCmd.Flags().String("title", "", "Filter by title text (case-insensitive substring match)") listCmd.Flags().IntP("limit", "n", 0, "Limit results") listCmd.Flags().String("format", "", "Output format: 'digraph' (for golang.org/x/tools/cmd/digraph), 'dot' (Graphviz), or Go template") rootCmd.AddCommand(listCmd) diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go index 0529d234..4a2d51e4 100644 --- a/internal/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -1151,6 +1151,12 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t args = append(args, pattern, pattern, pattern) } + if filter.TitleSearch != "" { + whereClauses = append(whereClauses, "title LIKE ?") + pattern := "%" + filter.TitleSearch + "%" + args = append(args, pattern) + } + if filter.Status != nil { whereClauses = append(whereClauses, "status = ?") args = append(args, *filter.Status) @@ -1171,6 +1177,14 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t args = append(args, *filter.Assignee) } + // Label filtering: issue must have ALL specified labels + if len(filter.Labels) > 0 { + for _, label := range filter.Labels { + whereClauses = append(whereClauses, "id IN (SELECT issue_id FROM labels WHERE label = ?)") + args = append(args, label) + } + } + whereSQL := "" if len(whereClauses) > 0 { whereSQL = "WHERE " + strings.Join(whereClauses, " AND ") diff --git a/internal/types/types.go b/internal/types/types.go index 38ff6608..0a797690 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -189,12 +189,13 @@ type Statistics struct { // IssueFilter is used to filter issue queries type IssueFilter struct { - Status *Status - Priority *int - IssueType *IssueType - Assignee *string - Labels []string - Limit int + Status *Status + Priority *int + IssueType *IssueType + Assignee *string + Labels []string + TitleSearch string + Limit int } // WorkFilter is used to filter ready work queries