diff --git a/pkg/cli/audit_comparison.go b/pkg/cli/audit_comparison.go index 0ce9d6e16fe..68c6a416cfb 100644 --- a/pkg/cli/audit_comparison.go +++ b/pkg/cli/audit_comparison.go @@ -268,11 +268,21 @@ func selectAuditComparisonBaseline(current ProcessedRun, candidates []auditCompa scoreAuditComparisonCandidate(current, &candidates[index]) } - sort.SliceStable(candidates, func(left, right int) bool { - if candidates[left].Score != candidates[right].Score { - return candidates[left].Score > candidates[right].Score + slices.SortStableFunc(candidates, func(left, right auditComparisonCandidate) int { + if left.Score != right.Score { + if left.Score > right.Score { + return -1 + } + return 1 + } + switch { + case left.Run.CreatedAt.After(right.Run.CreatedAt): + return -1 + case right.Run.CreatedAt.After(left.Run.CreatedAt): + return 1 + default: + return 0 } - return candidates[left].Run.CreatedAt.After(candidates[right].Run.CreatedAt) }) return &candidates[0] diff --git a/pkg/cli/audit_expanded.go b/pkg/cli/audit_expanded.go index fc37c9a20ce..18ca15c3641 100644 --- a/pkg/cli/audit_expanded.go +++ b/pkg/cli/audit_expanded.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strings" "time" @@ -402,11 +403,21 @@ func buildSafeOutputSummary(items []CreatedItemReport, chainMetrics SafeOutputCh Count: count, }) } - sort.Slice(summary.TypeDetails, func(i, j int) bool { - if summary.TypeDetails[i].Count == summary.TypeDetails[j].Count { - return summary.TypeDetails[i].Type < summary.TypeDetails[j].Type + slices.SortFunc(summary.TypeDetails, func(a, b SafeOutputTypeDetail) int { + if a.Count == b.Count { + switch { + case a.Type < b.Type: + return -1 + case a.Type > b.Type: + return 1 + default: + return 0 + } + } + if a.Count > b.Count { + return -1 } - return summary.TypeDetails[i].Count > summary.TypeDetails[j].Count + return 1 }) // Build human-readable summary string @@ -537,8 +548,14 @@ func buildMCPServerHealth(mcpToolUsage *MCPToolUsageData, mcpFailures []MCPFailu } // Sort servers by request count (highest first) - sort.Slice(health.Servers, func(i, j int) bool { - return health.Servers[i].RequestCount > health.Servers[j].RequestCount + slices.SortFunc(health.Servers, func(a, b MCPServerHealthDetail) int { + if a.RequestCount > b.RequestCount { + return -1 + } + if a.RequestCount < b.RequestCount { + return 1 + } + return 0 }) // Build summary string @@ -581,8 +598,14 @@ func buildSlowestToolCalls(calls []MCPToolCall, topN int) []MCPSlowestToolCall { } // Sort by duration descending - sort.Slice(withDuration, func(i, j int) bool { - return withDuration[i].duration > withDuration[j].duration + slices.SortFunc(withDuration, func(a, b callWithDuration) int { + if a.duration > b.duration { + return -1 + } + if a.duration < b.duration { + return 1 + } + return 0 }) // Take top N diff --git a/pkg/cli/compile_stats.go b/pkg/cli/compile_stats.go index d2fb339a157..bf5b8389151 100644 --- a/pkg/cli/compile_stats.go +++ b/pkg/cli/compile_stats.go @@ -4,7 +4,7 @@ import ( "fmt" "os" "path/filepath" - "sort" + "slices" "strconv" "github.com/github/gh-aw/pkg/console" @@ -252,8 +252,14 @@ func displayStatsTable(statsList []*WorkflowStats) { compileStatsLog.Printf("Displaying stats table: workflow_count=%d", len(statsList)) // Sort by file size (descending) - sort.Slice(statsList, func(i, j int) bool { - return statsList[i].FileSize > statsList[j].FileSize + slices.SortFunc(statsList, func(a, b *WorkflowStats) int { + if a.FileSize > b.FileSize { + return -1 + } + if a.FileSize < b.FileSize { + return 1 + } + return 0 }) // Calculate totals diff --git a/pkg/cli/deps_outdated.go b/pkg/cli/deps_outdated.go index dd1f818c397..b054a9dd8d6 100644 --- a/pkg/cli/deps_outdated.go +++ b/pkg/cli/deps_outdated.go @@ -6,7 +6,7 @@ import ( "io" "net/http" "os" - "sort" + "slices" "strings" "time" @@ -100,8 +100,15 @@ func DisplayOutdatedDependencies(outdated []OutdatedDependency, totalDeps int) { fmt.Fprintln(os.Stderr, "") // Sort by module name - sort.Slice(outdated, func(i, j int) bool { - return outdated[i].Module < outdated[j].Module + slices.SortFunc(outdated, func(a, b OutdatedDependency) int { + switch { + case a.Module < b.Module: + return -1 + case a.Module > b.Module: + return 1 + default: + return 0 + } }) // Display table diff --git a/pkg/cli/deps_security.go b/pkg/cli/deps_security.go index 682cb54474c..1f764a19fd3 100644 --- a/pkg/cli/deps_security.go +++ b/pkg/cli/deps_security.go @@ -7,7 +7,7 @@ import ( "io" "net/http" "os" - "sort" + "slices" "strings" "github.com/github/gh-aw/pkg/console" @@ -100,8 +100,16 @@ func DisplaySecurityAdvisories(advisories []SecurityAdvisory) { fmt.Fprintln(os.Stderr, "") // Sort by severity (critical first) - sort.Slice(advisories, func(i, j int) bool { - return severityWeight(advisories[i].Severity) > severityWeight(advisories[j].Severity) + slices.SortFunc(advisories, func(a, b SecurityAdvisory) int { + aw := severityWeight(a.Severity) + bw := severityWeight(b.Severity) + if aw > bw { + return -1 + } + if aw < bw { + return 1 + } + return 0 }) // Display each advisory diff --git a/pkg/cli/experiments_command.go b/pkg/cli/experiments_command.go index 1cea38af8e7..a91c8e86a15 100644 --- a/pkg/cli/experiments_command.go +++ b/pkg/cli/experiments_command.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strings" @@ -685,8 +686,15 @@ func experimentDetailsFromState(workflowID, branchName string, state *Experiment Total: total, }) } - sort.Slice(experiments, func(i, j int) bool { - return experiments[i].Name < experiments[j].Name + slices.SortFunc(experiments, func(a, b ExperimentVariantStats) int { + switch { + case a.Name < b.Name: + return -1 + case a.Name > b.Name: + return 1 + default: + return 0 + } }) recentRuns := state.Runs @@ -771,7 +779,16 @@ func printExperimentDetails(d *ExperimentDetails) { for k, v := range exp.Variants { pairs = append(pairs, kv{k, v}) } - sort.Slice(pairs, func(i, j int) bool { return pairs[i].k < pairs[j].k }) + slices.SortFunc(pairs, func(a, b kv) int { + switch { + case a.k < b.k: + return -1 + case a.k > b.k: + return 1 + default: + return 0 + } + }) for _, p := range pairs { pct := 0 if exp.Total > 0 { diff --git a/pkg/cli/firewall_policy.go b/pkg/cli/firewall_policy.go index 49ef6c96345..48ddea342b1 100644 --- a/pkg/cli/firewall_policy.go +++ b/pkg/cli/firewall_policy.go @@ -8,7 +8,6 @@ import ( "path/filepath" "regexp" "slices" - "sort" "strings" "github.com/github/gh-aw/pkg/logger" @@ -99,8 +98,14 @@ func loadPolicyManifest(manifestPath string) (*PolicyManifest, error) { } // Sort rules by order for deterministic matching - sort.Slice(manifest.Rules, func(i, j int) bool { - return manifest.Rules[i].Order < manifest.Rules[j].Order + slices.SortFunc(manifest.Rules, func(a, b PolicyRule) int { + if a.Order < b.Order { + return -1 + } + if a.Order > b.Order { + return 1 + } + return 0 }) firewallPolicyLog.Printf("Loaded policy manifest: version=%d, rules=%d, ssl_bump=%v, dlp=%v", diff --git a/pkg/cli/forecast.go b/pkg/cli/forecast.go index 679803965a4..dea39f0d1f0 100644 --- a/pkg/cli/forecast.go +++ b/pkg/cli/forecast.go @@ -18,6 +18,7 @@ import ( "os" "os/signal" "path/filepath" + "slices" "sort" "strings" "time" @@ -329,16 +330,22 @@ func RunForecast(config ForecastConfig) error { } // Sort results by Monte Carlo P50 (or point estimate when MC unavailable) descending. - sort.Slice(results, func(i, j int) bool { - pi := results[i].ProjectedAIC - if mc := results[i].MonteCarlo; mc != nil { + slices.SortFunc(results, func(a, b ForecastWorkflowResult) int { + pi := a.ProjectedAIC + if mc := a.MonteCarlo; mc != nil { pi = mc.P50ProjectedAIC } - pj := results[j].ProjectedAIC - if mc := results[j].MonteCarlo; mc != nil { + pj := b.ProjectedAIC + if mc := b.MonteCarlo; mc != nil { pj = mc.P50ProjectedAIC } - return pi > pj + if pi > pj { + return -1 + } + if pi < pj { + return 1 + } + return 0 }) output := ForecastResult{ @@ -837,11 +844,21 @@ func extractExperimentVariantStubs(cfg *workflow.FrontmatterConfig) []ForecastVa }) } } - sort.Slice(stubs, func(i, j int) bool { - if stubs[i].ExperimentName != stubs[j].ExperimentName { - return stubs[i].ExperimentName < stubs[j].ExperimentName + slices.SortFunc(stubs, func(a, b ForecastVariantResult) int { + if a.ExperimentName != b.ExperimentName { + if a.ExperimentName < b.ExperimentName { + return -1 + } + return 1 + } + switch { + case a.Variant < b.Variant: + return -1 + case a.Variant > b.Variant: + return 1 + default: + return 0 } - return stubs[i].Variant < stubs[j].Variant }) return stubs } @@ -1032,16 +1049,22 @@ func emitPartialForecastResults(results []ForecastWorkflowResult, config Forecas fmt.Sprintf("Forecast interrupted; emitting partial results for %d workflow(s) processed so far.", len(results)))) // Sort partial results by Monte Carlo P50 descending (mirrors the full-results sort). - sort.Slice(results, func(i, j int) bool { - pi := results[i].ProjectedAIC - if mc := results[i].MonteCarlo; mc != nil { + slices.SortFunc(results, func(a, b ForecastWorkflowResult) int { + pi := a.ProjectedAIC + if mc := a.MonteCarlo; mc != nil { pi = mc.P50ProjectedAIC } - pj := results[j].ProjectedAIC - if mc := results[j].MonteCarlo; mc != nil { + pj := b.ProjectedAIC + if mc := b.MonteCarlo; mc != nil { pj = mc.P50ProjectedAIC } - return pi > pj + if pi > pj { + return -1 + } + if pi < pj { + return 1 + } + return 0 }) output := ForecastResult{ diff --git a/pkg/cli/gateway_logs_mcp.go b/pkg/cli/gateway_logs_mcp.go index 82979331b79..9b4fcb28165 100644 --- a/pkg/cli/gateway_logs_mcp.go +++ b/pkg/cli/gateway_logs_mcp.go @@ -10,7 +10,7 @@ import ( "fmt" "os" "path/filepath" - "sort" + "slices" "strings" "time" @@ -234,15 +234,32 @@ func buildMCPSummaryStats(gatewayMetrics *GatewayMetrics, mcpData *MCPToolUsageD } // Sort summaries by server name, then tool name - sort.Slice(mcpData.Summary, func(i, j int) bool { - if mcpData.Summary[i].ServerName != mcpData.Summary[j].ServerName { - return mcpData.Summary[i].ServerName < mcpData.Summary[j].ServerName + slices.SortFunc(mcpData.Summary, func(a, b MCPToolSummary) int { + if a.ServerName != b.ServerName { + if a.ServerName < b.ServerName { + return -1 + } + return 1 + } + switch { + case a.ToolName < b.ToolName: + return -1 + case a.ToolName > b.ToolName: + return 1 + default: + return 0 } - return mcpData.Summary[i].ToolName < mcpData.Summary[j].ToolName }) // Sort servers by name - sort.Slice(mcpData.Servers, func(i, j int) bool { - return mcpData.Servers[i].ServerName < mcpData.Servers[j].ServerName + slices.SortFunc(mcpData.Servers, func(a, b MCPServerStats) int { + switch { + case a.ServerName < b.ServerName: + return -1 + case a.ServerName > b.ServerName: + return 1 + default: + return 0 + } }) } diff --git a/pkg/cli/gateway_logs_render.go b/pkg/cli/gateway_logs_render.go index 8b651191bed..5c64d40455e 100644 --- a/pkg/cli/gateway_logs_render.go +++ b/pkg/cli/gateway_logs_render.go @@ -7,7 +7,7 @@ package cli import ( "fmt" "os" - "sort" + "slices" "strconv" "strings" "time" @@ -157,8 +157,14 @@ func renderGatewayMetricsTable(metrics *GatewayMetrics, verbose bool) string { // Sort tools by call count toolNames := sliceutil.MapKeys(server.Tools) - sort.Slice(toolNames, func(i, j int) bool { - return server.Tools[toolNames[i]].CallCount > server.Tools[toolNames[j]].CallCount + slices.SortFunc(toolNames, func(a, b string) int { + if server.Tools[a].CallCount > server.Tools[b].CallCount { + return -1 + } + if server.Tools[a].CallCount < server.Tools[b].CallCount { + return 1 + } + return 0 }) toolRows := make([][]string, 0, len(toolNames)) @@ -187,8 +193,14 @@ func renderGatewayMetricsTable(metrics *GatewayMetrics, verbose bool) string { // getSortedServerNames returns server names sorted by request count func getSortedServerNames(metrics *GatewayMetrics) []string { names := sliceutil.MapKeys(metrics.Servers) - sort.Slice(names, func(i, j int) bool { - return metrics.Servers[names[i]].RequestCount > metrics.Servers[names[j]].RequestCount + slices.SortFunc(names, func(a, b string) int { + if metrics.Servers[a].RequestCount > metrics.Servers[b].RequestCount { + return -1 + } + if metrics.Servers[a].RequestCount < metrics.Servers[b].RequestCount { + return 1 + } + return 0 }) return names } diff --git a/pkg/cli/gateway_logs_timeline.go b/pkg/cli/gateway_logs_timeline.go index 9988bc22e8f..872036e2126 100644 --- a/pkg/cli/gateway_logs_timeline.go +++ b/pkg/cli/gateway_logs_timeline.go @@ -20,7 +20,7 @@ import ( "math" "os" "path/filepath" - "sort" + "slices" "strings" "time" @@ -547,8 +547,15 @@ func BuildUnifiedTimeline(logDir string, verbose bool) ([]UnifiedTimelineEvent, events = append(events, firewallEvents...) events = append(events, agentEvents...) - sort.Slice(events, func(i, j int) bool { - return events[i].Time.Before(events[j].Time) + slices.SortFunc(events, func(a, b UnifiedTimelineEvent) int { + switch { + case a.Time.Before(b.Time): + return -1 + case b.Time.Before(a.Time): + return 1 + default: + return 0 + } }) gatewayLogsLog.Printf("Built unified timeline: %d events (gateway=%d, firewall=%d, agent=%d)", diff --git a/pkg/cli/gateway_logs_timeline_render.go b/pkg/cli/gateway_logs_timeline_render.go index 73b20f12e9e..189d9f6757b 100644 --- a/pkg/cli/gateway_logs_timeline_render.go +++ b/pkg/cli/gateway_logs_timeline_render.go @@ -23,7 +23,7 @@ package cli import ( "fmt" "os" - "sort" + "slices" "strings" "github.com/github/gh-aw/pkg/console" @@ -711,13 +711,20 @@ func displayUnifiedTimeline(processedRuns []ProcessedRun, verbose bool) { // sortUnifiedTimelineEvents sorts events in-place by ascending wall-clock time. // It is a no-op when the slice is already sorted; otherwise it delegates to -// sort.SliceStable, which preserves insertion order for equal timestamps. +// slices.SortStableFunc, which preserves insertion order for equal timestamps. func sortUnifiedTimelineEvents(events []UnifiedTimelineEvent) { for i := 1; i < len(events); i++ { if events[i].Time.Before(events[i-1].Time) { // Only sort when the slice is not already in order. - sort.SliceStable(events, func(a, b int) bool { - return events[a].Time.Before(events[b].Time) + slices.SortStableFunc(events, func(a, b UnifiedTimelineEvent) int { + switch { + case a.Time.Before(b.Time): + return -1 + case b.Time.Before(a.Time): + return 1 + default: + return 0 + } }) return } diff --git a/pkg/cli/generate_action_metadata_command.go b/pkg/cli/generate_action_metadata_command.go index f761343f131..2d4b47a8495 100644 --- a/pkg/cli/generate_action_metadata_command.go +++ b/pkg/cli/generate_action_metadata_command.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strings" @@ -234,8 +235,15 @@ func extractInputs(content string) []ActionInput { } // Sort inputs by name for consistency - sort.Slice(inputs, func(i, j int) bool { - return inputs[i].Name < inputs[j].Name + slices.SortFunc(inputs, func(a, b ActionInput) int { + switch { + case a.Name < b.Name: + return -1 + case a.Name > b.Name: + return 1 + default: + return 0 + } }) return inputs @@ -263,8 +271,15 @@ func extractOutputs(content string) []ActionOutput { } // Sort outputs by name for consistency - sort.Slice(outputs, func(i, j int) bool { - return outputs[i].Name < outputs[j].Name + slices.SortFunc(outputs, func(a, b ActionOutput) int { + switch { + case a.Name < b.Name: + return -1 + case a.Name > b.Name: + return 1 + default: + return 0 + } }) return outputs diff --git a/pkg/cli/logs_github_rate_limit_usage.go b/pkg/cli/logs_github_rate_limit_usage.go index 6f2ff229987..84fe753f8e5 100644 --- a/pkg/cli/logs_github_rate_limit_usage.go +++ b/pkg/cli/logs_github_rate_limit_usage.go @@ -16,7 +16,7 @@ import ( "fmt" "os" "path/filepath" - "sort" + "slices" "strings" "github.com/github/gh-aw/pkg/console" @@ -67,8 +67,14 @@ type GitHubRateLimitUsage struct { func (u *GitHubRateLimitUsage) ResourceRows() []*GitHubRateLimitResourceUsage { rows := make([]*GitHubRateLimitResourceUsage, len(u.Resources)) copy(rows, u.Resources) - sort.Slice(rows, func(i, j int) bool { - return rows[i].RequestsMade > rows[j].RequestsMade + slices.SortFunc(rows, func(a, b *GitHubRateLimitResourceUsage) int { + if a.RequestsMade > b.RequestsMade { + return -1 + } + if a.RequestsMade < b.RequestsMade { + return 1 + } + return 0 }) return rows } @@ -247,8 +253,14 @@ func parseGitHubRateLimitsFile(filePath string) (*GitHubRateLimitUsage, error) { } // Sort resources for deterministic output - sort.Slice(usage.Resources, func(i, j int) bool { - return usage.Resources[i].RequestsMade > usage.Resources[j].RequestsMade + slices.SortFunc(usage.Resources, func(a, b *GitHubRateLimitResourceUsage) int { + if a.RequestsMade > b.RequestsMade { + return -1 + } + if a.RequestsMade < b.RequestsMade { + return 1 + } + return 0 }) return usage, nil diff --git a/pkg/cli/token_usage.go b/pkg/cli/token_usage.go index 9e5f4520dea..abf835ba1fa 100644 --- a/pkg/cli/token_usage.go +++ b/pkg/cli/token_usage.go @@ -10,7 +10,6 @@ import ( "path/filepath" "regexp" "slices" - "sort" "strings" "time" @@ -239,16 +238,30 @@ func extractAmbientContextMetrics(entries []TokenUsageEntry) *AmbientContextMetr }) } - sort.SliceStable(ordered, func(i, j int) bool { - left := ordered[i] - right := ordered[j] + slices.SortStableFunc(ordered, func(left, right orderedTokenEntry) int { if left.hasTimestamp && right.hasTimestamp { - return left.timestamp.Before(right.timestamp) + switch { + case left.timestamp.Before(right.timestamp): + return -1 + case right.timestamp.Before(left.timestamp): + return 1 + default: + return 0 + } } if left.hasTimestamp != right.hasTimestamp { - return left.hasTimestamp + if left.hasTimestamp { + return -1 + } + return 1 + } + if left.order < right.order { + return -1 } - return left.order < right.order + if left.order > right.order { + return 1 + } + return 0 }) firstCall := ordered[0].entry @@ -674,11 +687,21 @@ func augmentSubagentModelAttribution(runDir string, summary *TokenUsageSummary) }) observedModels[model] = usage.Provider } - sort.SliceStable(actuals, func(i, j int) bool { - if actuals[i].Requests != actuals[j].Requests { - return actuals[i].Requests > actuals[j].Requests + slices.SortStableFunc(actuals, func(a, b SubagentModelActual) int { + if a.Requests != b.Requests { + if a.Requests > b.Requests { + return -1 + } + return 1 + } + switch { + case a.Model < b.Model: + return -1 + case a.Model > b.Model: + return 1 + default: + return 0 } - return actuals[i].Model < actuals[j].Model }) summary.SubagentModelActuals = actuals @@ -772,11 +795,21 @@ func extractSubagentModelRequests(runDir string) []SubagentModelRequest { InvocationCount: n, }) } - sort.SliceStable(rows, func(i, j int) bool { - if rows[i].AgentName != rows[j].AgentName { - return rows[i].AgentName < rows[j].AgentName + slices.SortStableFunc(rows, func(a, b SubagentModelRequest) int { + if a.AgentName != b.AgentName { + if a.AgentName < b.AgentName { + return -1 + } + return 1 + } + switch { + case a.RequestedModel < b.RequestedModel: + return -1 + case a.RequestedModel > b.RequestedModel: + return 1 + default: + return 0 } - return rows[i].RequestedModel < rows[j].RequestedModel }) return rows } @@ -860,10 +893,16 @@ func (s *TokenUsageSummary) ModelRows() []ModelTokenUsageRow { }) } // Sort by total tokens descending - sort.Slice(rows, func(i, j int) bool { - iTot := rows[i].InputTokens + rows[i].OutputTokens + rows[i].CacheReadTokens + rows[i].CacheWriteTokens - jTot := rows[j].InputTokens + rows[j].OutputTokens + rows[j].CacheReadTokens + rows[j].CacheWriteTokens - return iTot > jTot + slices.SortFunc(rows, func(a, b ModelTokenUsageRow) int { + iTot := a.InputTokens + a.OutputTokens + a.CacheReadTokens + a.CacheWriteTokens + jTot := b.InputTokens + b.OutputTokens + b.CacheReadTokens + b.CacheWriteTokens + if iTot > jTot { + return -1 + } + if iTot < jTot { + return 1 + } + return 0 }) return rows } diff --git a/pkg/cli/tool_graph.go b/pkg/cli/tool_graph.go index c4b85ceee0a..ddf58c7d2bb 100644 --- a/pkg/cli/tool_graph.go +++ b/pkg/cli/tool_graph.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "os" + "slices" "sort" "strings" @@ -132,14 +133,27 @@ func (g *ToolGraph) GenerateMermaidGraph() string { } // Sort transitions by count (descending) for better visualization - sort.Slice(transitions, func(i, j int) bool { - if transitions[i].Count != transitions[j].Count { - return transitions[i].Count > transitions[j].Count + slices.SortFunc(transitions, func(a, b ToolTransition) int { + if a.Count != b.Count { + if a.Count > b.Count { + return -1 + } + return 1 + } + if a.From != b.From { + if a.From < b.From { + return -1 + } + return 1 } - if transitions[i].From != transitions[j].From { - return transitions[i].From < transitions[j].From + switch { + case a.To < b.To: + return -1 + case a.To > b.To: + return 1 + default: + return 0 } - return transitions[i].To < transitions[j].To }) for _, transition := range transitions { diff --git a/pkg/cli/update_actions.go b/pkg/cli/update_actions.go index 0d6bf5c7c27..0b9bbb78e35 100644 --- a/pkg/cli/update_actions.go +++ b/pkg/cli/update_actions.go @@ -8,7 +8,7 @@ import ( "os/exec" "path/filepath" "regexp" - "sort" + "slices" "strings" "time" @@ -341,8 +341,15 @@ func getLatestActionReleaseWithDeps(ctx context.Context, deps actionUpdateDeps, } // Sort releases by semver in descending order (highest first) - sort.Slice(validReleases, func(i, j int) bool { - return validReleases[i].version.IsNewer(validReleases[j].version) + slices.SortFunc(validReleases, func(a, b releaseWithVersion) int { + switch { + case a.version.IsNewer(b.version): + return -1 + case b.version.IsNewer(a.version): + return 1 + default: + return 0 + } }) // If current version is not valid, return the highest semver release @@ -468,8 +475,15 @@ func getLatestActionReleaseViaGit(ctx context.Context, repo, currentVersion stri } // Sort releases by semver in descending order (highest first) - sort.Slice(validReleases, func(i, j int) bool { - return validReleases[i].version.IsNewer(validReleases[j].version) + slices.SortFunc(validReleases, func(a, b releaseWithVersion) int { + switch { + case a.version.IsNewer(b.version): + return -1 + case b.version.IsNewer(a.version): + return 1 + default: + return 0 + } }) // If current version is not valid, return the highest semver release diff --git a/pkg/parser/schema_deprecation.go b/pkg/parser/schema_deprecation.go index 2a49ad16434..395d31166c6 100644 --- a/pkg/parser/schema_deprecation.go +++ b/pkg/parser/schema_deprecation.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "regexp" - "sort" + "slices" "strings" "sync" @@ -92,8 +92,15 @@ func extractDeprecatedFields(schemaDoc map[string]any) ([]DeprecatedField, error } // Sort by field name for consistent output - sort.Slice(deprecated, func(i, j int) bool { - return deprecated[i].Name < deprecated[j].Name + slices.SortFunc(deprecated, func(a, b DeprecatedField) int { + switch { + case a.Name < b.Name: + return -1 + case a.Name > b.Name: + return 1 + default: + return 0 + } }) return deprecated, nil @@ -164,8 +171,15 @@ func GetMainWorkflowDeprecatedFieldsDeep() ([]DeprecatedField, error) { } var fields []DeprecatedField collectDeprecatedDeep(schemaDoc, "", &fields) - sort.Slice(fields, func(i, j int) bool { - return fields[i].Path < fields[j].Path + slices.SortFunc(fields, func(a, b DeprecatedField) int { + switch { + case a.Path < b.Path: + return -1 + case a.Path > b.Path: + return 1 + default: + return 0 + } }) deprecatedFieldsDeepCache = fields schemaDeprecationLog.Printf("Found %d deprecated fields (deep) in main workflow schema", len(fields)) diff --git a/pkg/parser/schema_suggestions.go b/pkg/parser/schema_suggestions.go index 0441e85eee3..40897e36072 100644 --- a/pkg/parser/schema_suggestions.go +++ b/pkg/parser/schema_suggestions.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "regexp" + "slices" "sort" "strings" @@ -695,11 +696,21 @@ func findFieldLocationsInSchema(schemaDoc any, targetField, currentPath string) } // Sort fuzzy matches by distance (ascending), then path for stable output - sort.Slice(fuzzyMatches, func(i, j int) bool { - if fuzzyMatches[i].Distance != fuzzyMatches[j].Distance { - return fuzzyMatches[i].Distance < fuzzyMatches[j].Distance + slices.SortFunc(fuzzyMatches, func(a, b schemaFieldLocation) int { + if a.Distance != b.Distance { + if a.Distance < b.Distance { + return -1 + } + return 1 + } + switch { + case a.SchemaPath < b.SchemaPath: + return -1 + case a.SchemaPath > b.SchemaPath: + return 1 + default: + return 0 } - return fuzzyMatches[i].SchemaPath < fuzzyMatches[j].SchemaPath }) schemaSuggestionsLog.Printf("Found %d fuzzy schema locations for field '%s'", len(fuzzyMatches), targetField) diff --git a/pkg/stringutil/fuzzy_match.go b/pkg/stringutil/fuzzy_match.go index 79969f72b08..87a53914082 100644 --- a/pkg/stringutil/fuzzy_match.go +++ b/pkg/stringutil/fuzzy_match.go @@ -2,7 +2,7 @@ package stringutil import ( - "sort" + "slices" "strings" "github.com/github/gh-aw/pkg/logger" @@ -45,11 +45,21 @@ func FindClosestMatches(target string, candidates []string, maxResults int) []st } // Sort by distance (lower is better), then alphabetically for ties - sort.Slice(matches, func(i, j int) bool { - if matches[i].distance != matches[j].distance { - return matches[i].distance < matches[j].distance + slices.SortFunc(matches, func(a, b match) int { + if a.distance != b.distance { + if a.distance < b.distance { + return -1 + } + return 1 + } + switch { + case a.value < b.value: + return -1 + case a.value > b.value: + return 1 + default: + return 0 } - return matches[i].value < matches[j].value }) // Return top matches diff --git a/pkg/workflow/action_cache.go b/pkg/workflow/action_cache.go index d89cdd78127..afe32d415de 100644 --- a/pkg/workflow/action_cache.go +++ b/pkg/workflow/action_cache.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strings" "time" @@ -545,8 +546,15 @@ func buildDedupKeyInfos(keys []string) []cacheKeyInfo { } keyInfos[i] = cacheKeyInfo{key: key, versionRef: versionRef} } - sort.Slice(keyInfos, func(i, j int) bool { - return isMorePreciseVersion(keyInfos[i].versionRef, keyInfos[j].versionRef) + slices.SortFunc(keyInfos, func(a, b cacheKeyInfo) int { + switch { + case isMorePreciseVersion(a.versionRef, b.versionRef): + return -1 + case isMorePreciseVersion(b.versionRef, a.versionRef): + return 1 + default: + return 0 + } }) return keyInfos } diff --git a/pkg/workflow/agentic_engine.go b/pkg/workflow/agentic_engine.go index f6bc5ddb811..854f4fecd96 100644 --- a/pkg/workflow/agentic_engine.go +++ b/pkg/workflow/agentic_engine.go @@ -2,6 +2,7 @@ package workflow import ( "fmt" + "slices" "sort" "strings" "sync" @@ -628,7 +629,16 @@ func (r *EngineRegistry) GetEngineByPrefix(prefix string) (CodingAgentEngine, er agenticEngineLog.Printf("No engine found matching prefix: %s", prefix) return nil, fmt.Errorf("no engine found matching prefix: %s", prefix) } - sort.Slice(candidates, func(i, j int) bool { return candidates[i].id < candidates[j].id }) + slices.SortFunc(candidates, func(a, b engineCandidate) int { + switch { + case a.id < b.id: + return -1 + case a.id > b.id: + return 1 + default: + return 0 + } + }) agenticEngineLog.Printf("Found %d engine candidate(s) for prefix %s, using: %s", len(candidates), prefix, candidates[0].id) return candidates[0].engine, nil } diff --git a/pkg/workflow/central_slash_command_workflow.go b/pkg/workflow/central_slash_command_workflow.go index f463345a311..c2061425f8e 100644 --- a/pkg/workflow/central_slash_command_workflow.go +++ b/pkg/workflow/central_slash_command_workflow.go @@ -143,18 +143,29 @@ func collectCentralSlashCommandRoutes(workflowDataList []*WorkflowData) (map[str // Stable ordering for deterministic output. for commandName := range routesByCommand { - sort.Slice(routesByCommand[commandName], func(i, j int) bool { - left := routesByCommand[commandName][i] - right := routesByCommand[commandName][j] + slices.SortFunc(routesByCommand[commandName], func(left, right slashCommandRoute) int { if left.Workflow != right.Workflow { - return left.Workflow < right.Workflow + if left.Workflow < right.Workflow { + return -1 + } + return 1 } leftEvents := strings.Join(left.Events, ",") rightEvents := strings.Join(right.Events, ",") if leftEvents != rightEvents { - return leftEvents < rightEvents + if leftEvents < rightEvents { + return -1 + } + return 1 + } + switch { + case left.AIReaction < right.AIReaction: + return -1 + case left.AIReaction > right.AIReaction: + return 1 + default: + return 0 } - return left.AIReaction < right.AIReaction }) } @@ -194,18 +205,29 @@ func collectCentralLabelCommandRoutes(workflowDataList []*WorkflowData, mergedEv } for labelName := range routesByLabel { - sort.Slice(routesByLabel[labelName], func(i, j int) bool { - left := routesByLabel[labelName][i] - right := routesByLabel[labelName][j] + slices.SortFunc(routesByLabel[labelName], func(left, right slashCommandRoute) int { if left.Workflow != right.Workflow { - return left.Workflow < right.Workflow + if left.Workflow < right.Workflow { + return -1 + } + return 1 } leftEvents := strings.Join(left.Events, ",") rightEvents := strings.Join(right.Events, ",") if leftEvents != rightEvents { - return leftEvents < rightEvents + if leftEvents < rightEvents { + return -1 + } + return 1 + } + switch { + case left.AIReaction < right.AIReaction: + return -1 + case left.AIReaction > right.AIReaction: + return 1 + default: + return 0 } - return left.AIReaction < right.AIReaction }) } @@ -343,18 +365,29 @@ func writeCentralRouteTypeSummary(b *strings.Builder, routesByTrigger map[string for _, trigger := range triggers { routes := slices.Clone(routesByTrigger[trigger]) - sort.Slice(routes, func(i, j int) bool { - left := routes[i] - right := routes[j] + slices.SortFunc(routes, func(left, right slashCommandRoute) int { if left.Workflow != right.Workflow { - return left.Workflow < right.Workflow + if left.Workflow < right.Workflow { + return -1 + } + return 1 } leftEvents := strings.Join(left.Events, ",") rightEvents := strings.Join(right.Events, ",") if leftEvents != rightEvents { - return leftEvents < rightEvents + if leftEvents < rightEvents { + return -1 + } + return 1 + } + switch { + case left.AIReaction < right.AIReaction: + return -1 + case left.AIReaction > right.AIReaction: + return 1 + default: + return 0 } - return left.AIReaction < right.AIReaction }) for _, route := range routes { b.WriteString("# ") diff --git a/pkg/workflow/dependabot.go b/pkg/workflow/dependabot.go index 96f588e75ab..3ce349afae9 100644 --- a/pkg/workflow/dependabot.go +++ b/pkg/workflow/dependabot.go @@ -196,8 +196,15 @@ func (c *Compiler) collectNpmDependencies(workflowDataList []*WorkflowData) []Np } // Sort by name for deterministic output - sort.Slice(deps, func(i, j int) bool { - return deps[i].Name < deps[j].Name + slices.SortFunc(deps, func(a, b NpmDependency) int { + switch { + case a.Name < b.Name: + return -1 + case a.Name > b.Name: + return 1 + default: + return 0 + } }) dependabotLog.Printf("Collected %d unique dependencies", len(deps)) @@ -695,8 +702,15 @@ func (c *Compiler) collectPipDependencies(workflowDataList []*WorkflowData) []Pi } // Sort by name for deterministic output - sort.Slice(deps, func(i, j int) bool { - return deps[i].Name < deps[j].Name + slices.SortFunc(deps, func(a, b PipDependency) int { + switch { + case a.Name < b.Name: + return -1 + case a.Name > b.Name: + return 1 + default: + return 0 + } }) dependabotLog.Printf("Collected %d unique pip dependencies", len(deps)) @@ -832,8 +846,15 @@ func (c *Compiler) collectGoDependencies(workflowDataList []*WorkflowData) []GoD } // Sort by path for deterministic output - sort.Slice(deps, func(i, j int) bool { - return deps[i].Path < deps[j].Path + slices.SortFunc(deps, func(a, b GoDependency) int { + switch { + case a.Path < b.Path: + return -1 + case a.Path > b.Path: + return 1 + default: + return 0 + } }) dependabotLog.Printf("Collected %d unique Go dependencies", len(deps)) diff --git a/pkg/workflow/error_recovery.go b/pkg/workflow/error_recovery.go index 4a3c2de8fdb..51f85499594 100644 --- a/pkg/workflow/error_recovery.go +++ b/pkg/workflow/error_recovery.go @@ -2,7 +2,7 @@ package workflow import ( "errors" - "sort" + "slices" "strings" "github.com/github/gh-aw/pkg/logger" @@ -180,14 +180,27 @@ func prioritizeErrorMessages(messages []string) ([]PrioritizedError, int) { suppressedCount = 0 } - sort.SliceStable(prioritized, func(i, j int) bool { - if prioritized[i].Severity != prioritized[j].Severity { - return prioritized[i].Severity < prioritized[j].Severity + slices.SortStableFunc(prioritized, func(a, b PrioritizedError) int { + if a.Severity != b.Severity { + if a.Severity < b.Severity { + return -1 + } + return 1 + } + if a.Category != b.Category { + if a.Category < b.Category { + return -1 + } + return 1 } - if prioritized[i].Category != prioritized[j].Category { - return prioritized[i].Category < prioritized[j].Category + switch { + case a.Message < b.Message: + return -1 + case a.Message > b.Message: + return 1 + default: + return 0 } - return prioritized[i].Message < prioritized[j].Message }) return prioritized, suppressedCount diff --git a/pkg/workflow/expression_extraction.go b/pkg/workflow/expression_extraction.go index 3492f4e10b8..f90f270a5fa 100644 --- a/pkg/workflow/expression_extraction.go +++ b/pkg/workflow/expression_extraction.go @@ -6,7 +6,7 @@ import ( "fmt" "os" "regexp" - "sort" + "slices" "strings" "github.com/github/gh-aw/pkg/console" @@ -156,8 +156,15 @@ func (e *ExpressionExtractor) ExtractExpressions(markdown string) ([]*Expression } // Sort by original expression for deterministic output - sort.Slice(result, func(i, j int) bool { - return result[i].Original < result[j].Original + slices.SortFunc(result, func(a, b *ExpressionMapping) int { + switch { + case a.Original < b.Original: + return -1 + case a.Original > b.Original: + return 1 + default: + return 0 + } }) expressionExtractionLog.Printf("Extracted %d unique expressions", len(result)) @@ -410,8 +417,15 @@ func (e *ExpressionExtractor) ReplaceExpressionsWithEnvVars(markdown string) str for _, mapping := range e.mappings { mappings = append(mappings, mapping) } - sort.Slice(mappings, func(i, j int) bool { - return len(mappings[i].Original) > len(mappings[j].Original) + slices.SortFunc(mappings, func(a, b *ExpressionMapping) int { + switch { + case len(a.Original) > len(b.Original): + return -1 + case len(a.Original) < len(b.Original): + return 1 + default: + return 0 + } }) // Replace each expression with its environment variable reference diff --git a/pkg/workflow/metrics.go b/pkg/workflow/metrics.go index c60a02a6740..59c9851a967 100644 --- a/pkg/workflow/metrics.go +++ b/pkg/workflow/metrics.go @@ -2,7 +2,7 @@ package workflow import ( "encoding/json" - "sort" + "slices" "strings" "time" @@ -227,8 +227,15 @@ func FinalizeToolMetrics(opts FinalizeToolMetricsOptions) { } // Sort tool calls by name for consistent output - sort.Slice(opts.Metrics.ToolCalls, func(i, j int) bool { - return opts.Metrics.ToolCalls[i].Name < opts.Metrics.ToolCalls[j].Name + slices.SortFunc(opts.Metrics.ToolCalls, func(a, b ToolCallInfo) int { + switch { + case a.Name < b.Name: + return -1 + case a.Name > b.Name: + return 1 + default: + return 0 + } }) metricsLog.Printf("FinalizeToolMetrics: turns=%d, tokenUsage=%d, toolCalls=%d, sequences=%d", @@ -255,8 +262,15 @@ func FinalizeToolCallsAndSequence( } // Sort tool calls by name for consistent output - sort.Slice(metrics.ToolCalls, func(i, j int) bool { - return metrics.ToolCalls[i].Name < metrics.ToolCalls[j].Name + slices.SortFunc(metrics.ToolCalls, func(a, b ToolCallInfo) int { + switch { + case a.Name < b.Name: + return -1 + case a.Name > b.Name: + return 1 + default: + return 0 + } }) metricsLog.Printf("FinalizeToolCallsAndSequence: toolCalls=%d, sequences=%d", len(metrics.ToolCalls), len(metrics.ToolSequences)) diff --git a/pkg/workflow/permissions_operations.go b/pkg/workflow/permissions_operations.go index 5b9d8a52c4a..9ab13e79c65 100644 --- a/pkg/workflow/permissions_operations.go +++ b/pkg/workflow/permissions_operations.go @@ -3,6 +3,7 @@ package workflow import ( "fmt" "maps" + "slices" "sort" "strings" @@ -13,8 +14,15 @@ var permissionsOpsLog = logger.New("workflow:permissions_operations") // SortPermissionScopes sorts a slice of PermissionScope in place using Go's standard library sort func SortPermissionScopes(s []PermissionScope) { - sort.Slice(s, func(i, j int) bool { - return string(s[i]) < string(s[j]) + slices.SortFunc(s, func(a, b PermissionScope) int { + switch { + case string(a) < string(b): + return -1 + case string(a) > string(b): + return 1 + default: + return 0 + } }) } diff --git a/pkg/workflow/run_step_sanitizer.go b/pkg/workflow/run_step_sanitizer.go index ee7eeeab3c3..4cc5b0d4f2e 100644 --- a/pkg/workflow/run_step_sanitizer.go +++ b/pkg/workflow/run_step_sanitizer.go @@ -47,7 +47,7 @@ package workflow import ( "fmt" "maps" - "sort" + "slices" "strings" "github.com/github/gh-aw/pkg/logger" @@ -126,8 +126,15 @@ func sanitizeRunStepExpressions(step map[string]any) (map[string]any, []string, // Sort longest expressions first to avoid partial replacements when one // expression is a substring of another. - sort.Slice(ordered, func(i, j int) bool { - return len(ordered[i].Original) > len(ordered[j].Original) + slices.SortFunc(ordered, func(a, b sanitizedExpression) int { + switch { + case len(a.Original) > len(b.Original): + return -1 + case len(a.Original) < len(b.Original): + return 1 + default: + return 0 + } }) // Merge extracted env vars into a copy of the existing env: map. diff --git a/pkg/workflow/safe_update_manifest.go b/pkg/workflow/safe_update_manifest.go index 75288fc984a..7501a558de3 100644 --- a/pkg/workflow/safe_update_manifest.go +++ b/pkg/workflow/safe_update_manifest.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "regexp" + "slices" "sort" "strings" @@ -93,8 +94,15 @@ func NewGHAWManifest(secretNames []string, actionRefs []string, failures []GHAWM sortedContainers = append(sortedContainers, c) } } - sort.Slice(sortedContainers, func(i, j int) bool { - return sortedContainers[i].Image < sortedContainers[j].Image + slices.SortFunc(sortedContainers, func(a, b GHAWManifestContainer) int { + switch { + case a.Image < b.Image: + return -1 + case a.Image > b.Image: + return 1 + default: + return 0 + } }) safeUpdateManifestLog.Printf("Manifest built: version=%d, secrets=%d, actions=%d, containers=%d", @@ -165,11 +173,21 @@ func parseActionRefs(refs []string) []GHAWManifestAction { } // Sort for deterministic output. - sort.Slice(actions, func(i, j int) bool { - if actions[i].Repo != actions[j].Repo { - return actions[i].Repo < actions[j].Repo + slices.SortFunc(actions, func(a, b GHAWManifestAction) int { + if a.Repo != b.Repo { + if a.Repo < b.Repo { + return -1 + } + return 1 + } + switch { + case a.SHA < b.SHA: + return -1 + case a.SHA > b.SHA: + return 1 + default: + return 0 } - return actions[i].SHA < actions[j].SHA }) return actions @@ -201,14 +219,27 @@ func normalizeResolutionFailures(failures []GHAWManifestResolutionFailure) []GHA ErrorType: errorType, }) } - sort.Slice(normalized, func(i, j int) bool { - if normalized[i].Repo != normalized[j].Repo { - return normalized[i].Repo < normalized[j].Repo + slices.SortFunc(normalized, func(a, b GHAWManifestResolutionFailure) int { + if a.Repo != b.Repo { + if a.Repo < b.Repo { + return -1 + } + return 1 + } + if a.Ref != b.Ref { + if a.Ref < b.Ref { + return -1 + } + return 1 } - if normalized[i].Ref != normalized[j].Ref { - return normalized[i].Ref < normalized[j].Ref + switch { + case a.ErrorType < b.ErrorType: + return -1 + case a.ErrorType > b.ErrorType: + return 1 + default: + return 0 } - return normalized[i].ErrorType < normalized[j].ErrorType }) return normalized } diff --git a/pkg/workflow/template_injection_utils.go b/pkg/workflow/template_injection_utils.go index ebc3bcfa2c2..9bf0f7307f9 100644 --- a/pkg/workflow/template_injection_utils.go +++ b/pkg/workflow/template_injection_utils.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "regexp" - "sort" + "slices" "strings" ) @@ -178,8 +178,15 @@ func replaceOutsideQuotedHeredocs(s, old, new string) string { templateInjectionValidationLog.Printf("Replacing outside %d quoted heredoc region(s): replacing %q with %q", len(quotedRegions), old, new) // Sort regions by start position so we can walk left-to-right. - sort.Slice(quotedRegions, func(i, j int) bool { - return quotedRegions[i].start < quotedRegions[j].start + slices.SortFunc(quotedRegions, func(a, b region) int { + switch { + case a.start < b.start: + return -1 + case a.start > b.start: + return 1 + default: + return 0 + } }) var result strings.Builder