diff --git a/pkg/console/console.go b/pkg/console/console.go index b1e7d5a08ab..cb5dfa156a5 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -215,29 +215,7 @@ func RenderTable(config TableConfig) string { } dataRowCount := len(config.Rows) - - styleFunc := func(row, col int) lipgloss.Style { - if !ttyCheck() { - return lipgloss.NewStyle() - } - if row == table.HeaderRow { - headerStyle := styles.TableHeader - return headerStyle.PaddingLeft(1).PaddingRight(1) - } - if config.ShowTotal && len(config.TotalRow) > 0 && row == dataRowCount { - totalStyle := styles.TableTotal - return totalStyle.PaddingLeft(1).PaddingRight(1) - } - if row%2 == 0 { - cellStyle := styles.TableCell - return cellStyle.PaddingLeft(1).PaddingRight(1) - } - return lipgloss.NewStyle(). - Foreground(styles.ColorForeground). - Background(styles.ColorTableAltRow). - PaddingLeft(1). - PaddingRight(1) - } + styleFunc := buildTableStyleFunc(config, ttyCheck, dataRowCount) borderStyle := lipgloss.NewStyle() if ttyCheck() { @@ -257,6 +235,31 @@ func RenderTable(config TableConfig) string { return output.String() } +// buildTableStyleFunc returns the lipgloss style function used by RenderTable. +// config supplies the ShowTotal/TotalRow flags; ttyCheck detects terminal output; +// dataRowCount is the number of data rows (excluding any total row). +func buildTableStyleFunc(config TableConfig, ttyCheck func() bool, dataRowCount int) func(int, int) lipgloss.Style { + return func(row, col int) lipgloss.Style { + if !ttyCheck() { + return lipgloss.NewStyle() + } + if row == table.HeaderRow { + return styles.TableHeader.PaddingLeft(1).PaddingRight(1) + } + if config.ShowTotal && len(config.TotalRow) > 0 && row == dataRowCount { + return styles.TableTotal.PaddingLeft(1).PaddingRight(1) + } + if row%2 == 0 { + return styles.TableCell.PaddingLeft(1).PaddingRight(1) + } + return lipgloss.NewStyle(). + Foreground(styles.ColorForeground). + Background(styles.ColorTableAltRow). + PaddingLeft(1). + PaddingRight(1) + } +} + // FormatCommandMessage formats a command execution message func FormatCommandMessage(command string) string { return applyStyle(styles.Command, "$ ") + command diff --git a/pkg/github/label_objective_mapping.go b/pkg/github/label_objective_mapping.go index 3e5fcd8e618..54c87f72b9a 100644 --- a/pkg/github/label_objective_mapping.go +++ b/pkg/github/label_objective_mapping.go @@ -64,43 +64,58 @@ func (om *ObjectiveMapping) ComputeObjectiveValue(issueLabels []string) int { switch logic { case "sum": - total := 0 - for _, v := range matchingValues { - total += v - } - labelObjectiveMappingLog.Printf("Computed objective value via sum: labels=%v, value=%d", matchedLabels, total) - return total - + return om.computeValueSum(matchingValues, matchedLabels) case "first": - // Return first issue label that's in priority_labels - if len(om.PriorityLabels) > 0 { - for _, issueLabel := range issueLabels { - for _, priorityLabel := range om.PriorityLabels { - if strings.EqualFold(issueLabel, priorityLabel) { - normalizedIssue := strings.ToLower(strings.TrimSpace(issueLabel)) - if val, ok := om.LabelToValue[normalizedIssue]; ok { - labelObjectiveMappingLog.Printf("Computed objective value via issue label priority: label=%s, value=%d", issueLabel, val) - return val - } + return om.computeValueFirst(issueLabels, matchingValues, matchedLabels) + default: // "max" + return om.computeValueMax(matchingValues, matchedLabels) + } +} + +// computeValueSum adds all matching label values and logs the result. +func (om *ObjectiveMapping) computeValueSum(matchingValues []int, matchedLabels []string) int { + total := 0 + for _, v := range matchingValues { + total += v + } + labelObjectiveMappingLog.Printf("Computed objective value via sum: labels=%v, value=%d", matchedLabels, total) + return total +} + +// computeValueFirst returns the value for the first issue label that appears in PriorityLabels. +// It iterates issueLabels in their existing order and returns the value for the first one +// found in PriorityLabels; if none match, it falls back to the first value in matchingValues. +func (om *ObjectiveMapping) computeValueFirst(issueLabels []string, matchingValues []int, matchedLabels []string) int { + // Return first issue label that's in priority_labels + if len(om.PriorityLabels) > 0 { + for _, issueLabel := range issueLabels { + for _, priorityLabel := range om.PriorityLabels { + if strings.EqualFold(issueLabel, priorityLabel) { + normalizedIssue := strings.ToLower(strings.TrimSpace(issueLabel)) + if val, ok := om.LabelToValue[normalizedIssue]; ok { + labelObjectiveMappingLog.Printf("Computed objective value via issue label priority: label=%s, value=%d", issueLabel, val) + return val } } } } - // Fallback to first matching label - result := matchingValues[0] - labelObjectiveMappingLog.Printf("Computed objective value via first match: labels=%v, value=%d", matchedLabels, result) - return result + } + // Fallback to first matching label + result := matchingValues[0] + labelObjectiveMappingLog.Printf("Computed objective value via first match: labels=%v, value=%d", matchedLabels, result) + return result +} - default: // "max" - maxVal := matchingValues[0] - for _, v := range matchingValues { - if v > maxVal { - maxVal = v - } +// computeValueMax returns the highest value among all matching labels. +func (om *ObjectiveMapping) computeValueMax(matchingValues []int, matchedLabels []string) int { + maxVal := matchingValues[0] + for _, v := range matchingValues { + if v > maxVal { + maxVal = v } - labelObjectiveMappingLog.Printf("Computed objective value via max: labels=%v, value=%d", matchedLabels, maxVal) - return maxVal } + labelObjectiveMappingLog.Printf("Computed objective value via max: labels=%v, value=%d", matchedLabels, maxVal) + return maxVal } // DefaultObjectiveMapping returns the built-in default label-to-value mapping. diff --git a/pkg/parser/schema_suggestions.go b/pkg/parser/schema_suggestions.go index dcd14371da9..96f544f6fb1 100644 --- a/pkg/parser/schema_suggestions.go +++ b/pkg/parser/schema_suggestions.go @@ -650,10 +650,22 @@ func findFieldLocationsInSchema(schemaDoc any, targetField, currentPath string) allLocations := collectSchemaPropertyPaths(schemaDoc, "", 0) targetLower := strings.ToLower(targetField) - seen := make(map[string]struct { - }) + exactMatches := collectExactFieldMatches(allLocations, targetField, currentPath) + if len(exactMatches) > 0 { + schemaSuggestionsLog.Printf("Found %d exact schema locations for field '%s'", len(exactMatches), targetField) + return exactMatches + } - // Collect exact matches first + fuzzyMatches := collectFuzzyFieldMatches(allLocations, targetLower, currentPath) + schemaSuggestionsLog.Printf("Found %d fuzzy schema locations for field '%s'", len(fuzzyMatches), targetField) + return fuzzyMatches +} + +// collectExactFieldMatches returns all locations in allLocations where the field +// name case-insensitively equals targetField, excluding the currentPath location. +// Duplicate (fieldName, schemaPath) pairs are suppressed. +func collectExactFieldMatches(allLocations []schemaFieldLocation, targetField, currentPath string) []schemaFieldLocation { + seen := make(map[string]struct{}) var exactMatches []schemaFieldLocation for _, loc := range allLocations { if loc.SchemaPath == currentPath { @@ -663,43 +675,36 @@ func findFieldLocationsInSchema(schemaDoc any, targetField, currentPath string) if setutil.Contains(seen, key) { continue } - seen[key] = struct { - }{} - + seen[key] = struct{}{} if strings.EqualFold(loc.FieldName, targetField) { loc.Distance = 0 exactMatches = append(exactMatches, loc) } } + return exactMatches +} - if len(exactMatches) > 0 { - schemaSuggestionsLog.Printf("Found %d exact schema locations for field '%s'", len(exactMatches), targetField) - return exactMatches - } - - // Fall back to fuzzy matching with a stricter distance threshold for high confidence - seenFuzzy := make(map[string]struct { - }) +// collectFuzzyFieldMatches returns locations whose field names are within +// maxPathSearchDistance Levenshtein edits of targetLower, excluding currentPath. +// Results are sorted by distance ascending, then by schema path for stable output. +func collectFuzzyFieldMatches(allLocations []schemaFieldLocation, targetLower, currentPath string) []schemaFieldLocation { + seen := make(map[string]struct{}) var fuzzyMatches []schemaFieldLocation for _, loc := range allLocations { if loc.SchemaPath == currentPath { continue } key := loc.FieldName + "|" + loc.SchemaPath - if setutil.Contains(seenFuzzy, key) { + if setutil.Contains(seen, key) { continue } - seenFuzzy[key] = struct { - }{} - + seen[key] = struct{}{} dist := LevenshteinDistance(targetLower, strings.ToLower(loc.FieldName)) if dist > 0 && dist <= maxPathSearchDistance { loc.Distance = dist fuzzyMatches = append(fuzzyMatches, loc) } } - - // Sort fuzzy matches by distance (ascending), then path for stable output slices.SortFunc(fuzzyMatches, func(a, b schemaFieldLocation) int { if a.Distance != b.Distance { if a.Distance < b.Distance { @@ -716,8 +721,6 @@ func findFieldLocationsInSchema(schemaDoc any, targetField, currentPath string) return 0 } }) - - schemaSuggestionsLog.Printf("Found %d fuzzy schema locations for field '%s'", len(fuzzyMatches), targetField) return fuzzyMatches }