From e67712dcd41256bd3975392890455c9704d9a4ab Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 22 Dec 2025 15:39:55 -0800 Subject: [PATCH] refactor(cmd): migrate sort.Slice to slices.SortFunc (bd-u2sc.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modernize sorting code to use Go 1.21+ slices package: - Replace sort.Slice with slices.SortFunc across 16 files - Use cmp.Compare for orderable types (strings, ints) - Use time.Time.Compare for time comparisons - Use cmp.Or for multi-field sorting - Use slices.SortStableFunc where stability matters Benefits: cleaner 3-way comparison, slightly better performance, modern idiomatic Go. Part of GH#692 refactoring epic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/autoflush.go | 11 +++++----- cmd/bd/count.go | 14 ++++++++----- cmd/bd/daemon_sync.go | 7 ++++--- cmd/bd/doctor.go | 14 ++++++------- cmd/bd/export.go | 9 ++++---- cmd/bd/import.go | 7 ++++--- cmd/bd/integrity.go | 7 ++++--- cmd/bd/jira.go | 7 ++++--- cmd/bd/list.go | 43 +++++++++++++++++++------------------- cmd/bd/migrate_hash_ids.go | 11 +++++----- cmd/bd/rename_prefix.go | 13 ++++++------ cmd/bd/show.go | 6 +++--- cmd/bd/sync.go | 7 ++++--- cmd/bd/thanks.go | 7 ++++--- cmd/bd/tips.go | 7 ++++--- cmd/bd/wisp.go | 6 +++--- 16 files changed, 96 insertions(+), 80 deletions(-) diff --git a/cmd/bd/autoflush.go b/cmd/bd/autoflush.go index a4d21931..2f7eba50 100644 --- a/cmd/bd/autoflush.go +++ b/cmd/bd/autoflush.go @@ -3,6 +3,7 @@ package main import ( "bufio" "bytes" + "cmp" "context" "crypto/sha256" "encoding/hex" @@ -10,7 +11,7 @@ import ( "fmt" "os" "path/filepath" - "sort" + "slices" "strings" "time" @@ -218,8 +219,8 @@ func autoImportIfNewer() { for oldID, newID := range result.IDMapping { mappings = append(mappings, mapping{oldID, newID}) } - sort.Slice(mappings, func(i, j int) bool { - return mappings[i].oldID < mappings[j].oldID + slices.SortFunc(mappings, func(a, b mapping) int { + return cmp.Compare(a.oldID, b.oldID) }) maxShow := 10 @@ -442,8 +443,8 @@ func validateJSONLIntegrity(ctx context.Context, jsonlPath string) (bool, error) func writeJSONLAtomic(jsonlPath string, issues []*types.Issue) ([]string, error) { // Sort issues by ID for consistent output - sort.Slice(issues, func(i, j int) bool { - return issues[i].ID < issues[j].ID + slices.SortFunc(issues, func(a, b *types.Issue) int { + return cmp.Compare(a.ID, b.ID) }) // Create temp file with PID suffix to avoid collisions (bd-306) diff --git a/cmd/bd/count.go b/cmd/bd/count.go index d28d7543..2b7ed4fb 100644 --- a/cmd/bd/count.go +++ b/cmd/bd/count.go @@ -1,10 +1,11 @@ package main import ( + "cmp" "encoding/json" "fmt" "os" - "sort" + "slices" "strings" "github.com/spf13/cobra" @@ -205,8 +206,11 @@ Examples: outputJSON(result) } else { // Sort groups for consistent output - sort.Slice(result.Groups, func(i, j int) bool { - return result.Groups[i].Group < result.Groups[j].Group + slices.SortFunc(result.Groups, func(a, b struct { + Group string `json:"group"` + Count int `json:"count"` + }) int { + return cmp.Compare(a.Group, b.Group) }) fmt.Printf("Total: %d\n\n", result.Total) @@ -397,8 +401,8 @@ Examples: } // Sort for consistent output - sort.Slice(groups, func(i, j int) bool { - return groups[i].Group < groups[j].Group + slices.SortFunc(groups, func(a, b GroupCount) int { + return cmp.Compare(a.Group, b.Group) }) if jsonOutput { diff --git a/cmd/bd/daemon_sync.go b/cmd/bd/daemon_sync.go index 08e33522..aac41f5b 100644 --- a/cmd/bd/daemon_sync.go +++ b/cmd/bd/daemon_sync.go @@ -2,12 +2,13 @@ package main import ( "bufio" + "cmp" "context" "encoding/json" "fmt" "os" "path/filepath" - "sort" + "slices" "strings" "time" @@ -59,8 +60,8 @@ func exportToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPat } // Sort by ID for consistent output - sort.Slice(issues, func(i, j int) bool { - return issues[i].ID < issues[j].ID + slices.SortFunc(issues, func(a, b *types.Issue) int { + return cmp.Compare(a.ID, b.ID) }) // Populate dependencies for all issues diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index fecefb2f..4a84c20b 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -7,7 +7,7 @@ import ( "fmt" "os" "path/filepath" - "sort" + "slices" "strings" "time" @@ -895,15 +895,15 @@ func printDiagnostics(result doctorResult) { fmt.Println(ui.RenderWarn(ui.IconWarn + " WARNINGS")) // Sort by severity: errors first, then warnings - sort.Slice(warnings, func(i, j int) bool { + slices.SortStableFunc(warnings, func(a, b doctorCheck) int { // Errors (statusError) come before warnings (statusWarning) - if warnings[i].Status == statusError && warnings[j].Status != statusError { - return true + if a.Status == statusError && b.Status != statusError { + return -1 } - if warnings[i].Status != statusError && warnings[j].Status == statusError { - return false + if a.Status != statusError && b.Status == statusError { + return 1 } - return false // maintain original order within same severity + return 0 // maintain original order within same severity }) for i, check := range warnings { diff --git a/cmd/bd/export.go b/cmd/bd/export.go index 61d8abb2..ac008f39 100644 --- a/cmd/bd/export.go +++ b/cmd/bd/export.go @@ -1,13 +1,14 @@ package main import ( + "cmp" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "os" "path/filepath" - "sort" + "slices" "strings" "github.com/spf13/cobra" @@ -316,7 +317,7 @@ Examples: len(jsonlIDs), len(issues), len(missingIDs)) if len(missingIDs) > 0 { - sort.Strings(missingIDs) + slices.Sort(missingIDs) fmt.Fprintf(os.Stderr, "Error: refusing to export stale database that would lose issues\n") fmt.Fprintf(os.Stderr, " Database has %d issues\n", len(issues)) fmt.Fprintf(os.Stderr, " JSONL has %d issues\n", len(jsonlIDs)) @@ -357,8 +358,8 @@ Examples: issues = filtered // Sort by ID for consistent output - sort.Slice(issues, func(i, j int) bool { - return issues[i].ID < issues[j].ID + slices.SortFunc(issues, func(a, b *types.Issue) int { + return cmp.Compare(a.ID, b.ID) }) // Populate dependencies for all issues in one query (avoids N+1 problem) diff --git a/cmd/bd/import.go b/cmd/bd/import.go index c9699b7e..634272b2 100644 --- a/cmd/bd/import.go +++ b/cmd/bd/import.go @@ -3,12 +3,13 @@ package main import ( "bufio" "bytes" + "cmp" "encoding/json" "fmt" "os" "os/exec" "path/filepath" - "sort" + "slices" "strings" "time" @@ -359,8 +360,8 @@ NOTE: Import requires direct database access and does not work with daemon mode. for oldID, newID := range result.IDMapping { mappings = append(mappings, mapping{oldID, newID}) } - sort.Slice(mappings, func(i, j int) bool { - return mappings[i].oldID < mappings[j].oldID + slices.SortFunc(mappings, func(a, b mapping) int { + return cmp.Compare(a.oldID, b.oldID) }) fmt.Fprintf(os.Stderr, "Remappings:\n") diff --git a/cmd/bd/integrity.go b/cmd/bd/integrity.go index bb33dc6c..1bdbdbc5 100644 --- a/cmd/bd/integrity.go +++ b/cmd/bd/integrity.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "cmp" "context" "crypto/sha256" "database/sql" @@ -10,7 +11,7 @@ import ( "fmt" "os" "path/filepath" - "sort" + "slices" "strings" "github.com/steveyegge/beads/internal/storage" @@ -431,8 +432,8 @@ func computeDBHash(ctx context.Context, store storage.Storage) (string, error) { } // Sort by ID for consistent hash - sort.Slice(issues, func(i, j int) bool { - return issues[i].ID < issues[j].ID + slices.SortFunc(issues, func(a, b *types.Issue) int { + return cmp.Compare(a.ID, b.ID) }) // Populate dependencies diff --git a/cmd/bd/jira.go b/cmd/bd/jira.go index d3de27ce..fd36a0d7 100644 --- a/cmd/bd/jira.go +++ b/cmd/bd/jira.go @@ -2,13 +2,14 @@ package main import ( "bufio" + "cmp" "context" "encoding/json" "fmt" "os" "os/exec" "path/filepath" - "sort" + "slices" "strings" "time" @@ -479,8 +480,8 @@ func doPushToJira(ctx context.Context, dryRun bool, createOnly bool, updateRefs } // Sort by ID for consistent output - sort.Slice(issues, func(i, j int) bool { - return issues[i].ID < issues[j].ID + slices.SortFunc(issues, func(a, b *types.Issue) int { + return cmp.Compare(a.ID, b.ID) }) // Generate JSONL for export diff --git a/cmd/bd/list.go b/cmd/bd/list.go index 9e4cd3f4..ac166587 100644 --- a/cmd/bd/list.go +++ b/cmd/bd/list.go @@ -2,11 +2,12 @@ package main import ( "bytes" + "cmp" "context" "encoding/json" "fmt" "os" - "sort" + "slices" "strings" "text/template" "time" @@ -53,50 +54,50 @@ func sortIssues(issues []*types.Issue, sortBy string, reverse bool) { return } - sort.Slice(issues, func(i, j int) bool { - var less bool + slices.SortFunc(issues, func(a, b *types.Issue) int { + var result int switch sortBy { case "priority": // Lower priority numbers come first (P0 > P1 > P2 > P3 > P4) - less = issues[i].Priority < issues[j].Priority + result = cmp.Compare(a.Priority, b.Priority) case "created": // Default: newest first (descending) - less = issues[i].CreatedAt.After(issues[j].CreatedAt) + result = b.CreatedAt.Compare(a.CreatedAt) case "updated": // Default: newest first (descending) - less = issues[i].UpdatedAt.After(issues[j].UpdatedAt) + result = b.UpdatedAt.Compare(a.UpdatedAt) case "closed": // Default: newest first (descending) // Handle nil ClosedAt values - if issues[i].ClosedAt == nil && issues[j].ClosedAt == nil { - less = false - } else if issues[i].ClosedAt == nil { - less = false // nil sorts last - } else if issues[j].ClosedAt == nil { - less = true // non-nil sorts before nil + if a.ClosedAt == nil && b.ClosedAt == nil { + result = 0 + } else if a.ClosedAt == nil { + result = 1 // nil sorts last + } else if b.ClosedAt == nil { + result = -1 // non-nil sorts before nil } else { - less = issues[i].ClosedAt.After(*issues[j].ClosedAt) + result = b.ClosedAt.Compare(*a.ClosedAt) } case "status": - less = issues[i].Status < issues[j].Status + result = cmp.Compare(a.Status, b.Status) case "id": - less = issues[i].ID < issues[j].ID + result = cmp.Compare(a.ID, b.ID) case "title": - less = strings.ToLower(issues[i].Title) < strings.ToLower(issues[j].Title) + result = cmp.Compare(strings.ToLower(a.Title), strings.ToLower(b.Title)) case "type": - less = issues[i].IssueType < issues[j].IssueType + result = cmp.Compare(a.IssueType, b.IssueType) case "assignee": - less = issues[i].Assignee < issues[j].Assignee + result = cmp.Compare(a.Assignee, b.Assignee) default: // Unknown sort field, no sorting - less = false + result = 0 } if reverse { - return !less + return -result } - return less + return result }) } diff --git a/cmd/bd/migrate_hash_ids.go b/cmd/bd/migrate_hash_ids.go index c5e17b0d..8c47b15f 100644 --- a/cmd/bd/migrate_hash_ids.go +++ b/cmd/bd/migrate_hash_ids.go @@ -1,6 +1,7 @@ package main import ( + "cmp" "context" "crypto/sha256" "encoding/hex" @@ -9,7 +10,7 @@ import ( "os" "path/filepath" "regexp" - "sort" + "slices" "strings" "time" @@ -272,8 +273,8 @@ func migrateToHashIDs(ctx context.Context, store *sqlite.SQLiteStorage, issues [ // We need to also update text references in descriptions, notes, design, acceptance criteria // Sort issues by ID to process parents before children - sort.Slice(issues, func(i, j int) bool { - return issues[i].ID < issues[j].ID + slices.SortFunc(issues, func(a, b *types.Issue) int { + return cmp.Compare(a.ID, b.ID) }) // Update all issues @@ -394,8 +395,8 @@ func saveMappingFile(path string, mapping map[string]string) error { } // Sort by old ID for readability - sort.Slice(entries, func(i, j int) bool { - return entries[i].OldID < entries[j].OldID + slices.SortFunc(entries, func(a, b mappingEntry) int { + return cmp.Compare(a.OldID, b.OldID) }) data, err := json.MarshalIndent(map[string]interface{}{ diff --git a/cmd/bd/rename_prefix.go b/cmd/bd/rename_prefix.go index bf9bba10..a39e63cb 100644 --- a/cmd/bd/rename_prefix.go +++ b/cmd/bd/rename_prefix.go @@ -1,13 +1,14 @@ package main import ( + "cmp" "context" "database/sql" "encoding/json" "fmt" "os" "regexp" - "sort" + "slices" "strings" "time" @@ -247,11 +248,11 @@ func repairPrefixes(ctx context.Context, st storage.Storage, actorName string, t } // Sort incorrect issues: first by prefix lexicographically, then by number - sort.Slice(incorrectIssues, func(i, j int) bool { - if incorrectIssues[i].prefix != incorrectIssues[j].prefix { - return incorrectIssues[i].prefix < incorrectIssues[j].prefix - } - return incorrectIssues[i].number < incorrectIssues[j].number + slices.SortFunc(incorrectIssues, func(a, b issueSort) int { + return cmp.Or( + cmp.Compare(a.prefix, b.prefix), + cmp.Compare(a.number, b.number), + ) }) // Get a database connection for ID generation diff --git a/cmd/bd/show.go b/cmd/bd/show.go index 108ee2b3..3195c9dd 100644 --- a/cmd/bd/show.go +++ b/cmd/bd/show.go @@ -6,7 +6,7 @@ import ( "fmt" "os" "os/exec" - "sort" + "slices" "strings" "github.com/spf13/cobra" @@ -1204,8 +1204,8 @@ func showMessageThread(ctx context.Context, messageID string, jsonOutput bool) { } // Sort by creation time - sort.Slice(threadMessages, func(i, j int) bool { - return threadMessages[i].CreatedAt.Before(threadMessages[j].CreatedAt) + slices.SortFunc(threadMessages, func(a, b *types.Issue) int { + return a.CreatedAt.Compare(b.CreatedAt) }) if jsonOutput { diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index 82deb7ff..c4e5521c 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -3,13 +3,14 @@ package main import ( "bufio" "bytes" + "cmp" "context" "encoding/json" "fmt" "os" "os/exec" "path/filepath" - "sort" + "slices" "strings" "time" @@ -1346,8 +1347,8 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error { } // Sort by ID for consistent output - sort.Slice(issues, func(i, j int) bool { - return issues[i].ID < issues[j].ID + slices.SortFunc(issues, func(a, b *types.Issue) int { + return cmp.Compare(a.ID, b.ID) }) // Populate dependencies for all issues (avoid N+1) diff --git a/cmd/bd/thanks.go b/cmd/bd/thanks.go index d1774128..735e2a2c 100644 --- a/cmd/bd/thanks.go +++ b/cmd/bd/thanks.go @@ -1,8 +1,9 @@ package main import ( + "cmp" "fmt" - "sort" + "slices" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" @@ -130,8 +131,8 @@ func getContributorsSorted() []string { for name, commits := range beadsContributors { sorted = append(sorted, kv{name, commits}) } - sort.Slice(sorted, func(i, j int) bool { - return sorted[i].commits > sorted[j].commits + slices.SortFunc(sorted, func(a, b kv) int { + return cmp.Compare(b.commits, a.commits) // descending order }) names := make([]string, len(sorted)) for i, kv := range sorted { diff --git a/cmd/bd/tips.go b/cmd/bd/tips.go index 11e66100..19659404 100644 --- a/cmd/bd/tips.go +++ b/cmd/bd/tips.go @@ -1,13 +1,14 @@ package main import ( + "cmp" "context" "encoding/json" "fmt" "math/rand" "os" "path/filepath" - "sort" + "slices" "strconv" "strings" "sync" @@ -116,8 +117,8 @@ func selectNextTip(store storage.Storage) *Tip { } // Sort by priority (highest first) - sort.Slice(eligibleTips, func(i, j int) bool { - return eligibleTips[i].Priority > eligibleTips[j].Priority + slices.SortFunc(eligibleTips, func(a, b Tip) int { + return cmp.Compare(b.Priority, a.Priority) // descending order }) // Apply probability roll (in priority order) diff --git a/cmd/bd/wisp.go b/cmd/bd/wisp.go index 2a68c9c0..173b4d65 100644 --- a/cmd/bd/wisp.go +++ b/cmd/bd/wisp.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "os" - "sort" + "slices" "strings" "time" @@ -397,8 +397,8 @@ func runWispList(cmd *cobra.Command, args []string) { } // Sort by updated_at descending (most recent first) - sort.Slice(items, func(i, j int) bool { - return items[i].UpdatedAt.After(items[j].UpdatedAt) + slices.SortFunc(items, func(a, b WispListItem) int { + return b.UpdatedAt.Compare(a.UpdatedAt) // descending order }) result := WispListResult{