package quality import ( "fmt" "time" ) // Scorer calculates quality scores and generates scorecards type Scorer struct { targetScore int } // NewScorer creates a new scorer with the given target score func NewScorer(targetScore int) *Scorer { if targetScore <= 0 { targetScore = 95 // Default target } return &Scorer{ targetScore: targetScore, } } // CalculateScore calculates the quality score from findings func (s *Scorer) CalculateScore(findings []Finding) (int, int) { totalScore := 0 strictScore := 0 for _, finding := range findings { weight := int(finding.Severity) score := finding.Score * weight totalScore += score // Strict score includes open and wontfix findings if finding.Status == StatusOpen || finding.Status == StatusWontfix { strictScore += score } } return totalScore, strictScore } // GenerateScorecard creates a scorecard from scan results func (s *Scorer) GenerateScorecard(findings []Finding, lastScan time.Time) *Scorecard { totalScore, strictScore := s.CalculateScore(findings) // Group findings by type and tier findingsByType := make(map[string]int) findingsByTier := make(map[Severity]int) statusByType := make(map[string]int) for _, finding := range findings { findingsByType[finding.Type]++ findingsByTier[finding.Severity]++ statusByType[string(finding.Status)]++ } return &Scorecard{ TotalScore: totalScore, StrictScore: strictScore, TargetScore: s.targetScore, FindingsByType: findingsByType, FindingsByTier: findingsByTier, StatusByType: statusByType, LastScan: lastScan, } } // GetHealthGrade returns a health grade based on score func (s *Scorer) GetHealthGrade(score int) string { percentage := s.getScorePercentage(score) switch { case percentage >= 90: return "A" case percentage >= 80: return "B" case percentage >= 70: return "C" case percentage >= 60: return "D" default: return "F" } } // getScorePercentage converts score to percentage (inverted - lower is better) func (s *Scorer) getScorePercentage(score int) int { // Invert score so lower debt = higher percentage maxPossibleScore := 1000 // Arbitrary high value for normalization percentage := 100 - (score * 100 / maxPossibleScore) if percentage < 0 { percentage = 0 } return percentage } // FormatScorecard formats the scorecard for display func (s *Scorer) FormatScorecard(card *Scorecard) string { grade := s.GetHealthGrade(card.StrictScore) percentage := s.getScorePercentage(card.StrictScore) output := fmt.Sprintf(` Code Quality Scorecard ======================================= Overall Health: %s (%d%%) Target Score: %d Current Score: %d (strict: %d) Findings by Type: `, grade, percentage, card.TargetScore, card.TotalScore, card.StrictScore) for ftype, count := range card.FindingsByType { output += fmt.Sprintf(" - %s: %d\n", ftype, count) } output += "\nFindings by Severity:\n" tierNames := map[Severity]string{ SeverityT1: "T1 (Auto-fixable)", SeverityT2: "T2 (Quick manual)", SeverityT3: "T3 (Needs judgment)", SeverityT4: "T4 (Major refactor)", } for severity, count := range card.FindingsByTier { if name, ok := tierNames[severity]; ok { output += fmt.Sprintf(" - %s: %d\n", name, count) } } output += "\nStatus Breakdown:\n" for status, count := range card.StatusByType { output += fmt.Sprintf(" - %s: %d\n", status, count) } output += fmt.Sprintf("\nLast Scan: %s\n", card.LastScan.Format("2006-01-02 15:04:05")) return output } // GetNextPriority returns the next highest priority finding to fix func (s *Scorer) GetNextPriority(findings []Finding) *Finding { if len(findings) == 0 { return nil } var highest *Finding highestWeight := 0 for _, finding := range findings { if finding.Status != StatusOpen { continue } weight := int(finding.Severity) * finding.Score if weight > highestWeight { highestWeight = weight highest = &finding } } return highest } // GetFindingsByTier returns findings grouped by severity tier func (s *Scorer) GetFindingsByTier(findings []Finding) map[Severity][]Finding { result := make(map[Severity][]Finding) for _, finding := range findings { if finding.Status == StatusOpen { result[finding.Severity] = append(result[finding.Severity], finding) } } return result } // GetProgressMetrics returns progress metrics for the scan func (s *Scorer) GetProgressMetrics(findings []Finding) map[string]interface{} { total := len(findings) open := 0 fixed := 0 wontfix := 0 for _, finding := range findings { switch finding.Status { case StatusOpen: open++ case StatusFixed: fixed++ case StatusWontfix: wontfix++ } } return map[string]interface{}{ "total": total, "open": open, "fixed": fixed, "wontfix": wontfix, "progress": float64(fixed) / float64(total) * 100, } }