Files
Tomas Dvorak 409acd2e08 updage
2026-02-22 15:41:27 +01:00

488 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package quality
import (
"fmt"
"strings"
"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: ONLY includes truly unresolved issues
// Excludes: fixed, false_positive, ignored, wontfix (if justified)
if s.isStrictlyRelevant(finding) {
// Apply strict multiplier for severity
strictMultiplier := s.getStrictMultiplier(finding)
strictScore += score * strictMultiplier
}
}
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,
}
}
// isStrictlyRelevant determines if a finding should count in strict scoring
func (s *Scorer) isStrictlyRelevant(finding Finding) bool {
switch finding.Status {
case StatusOpen:
return true
case StatusFixed:
return false // Already resolved
case StatusFalsePositive:
return false // Not a real issue
case StatusIgnored:
return false // Explicitly ignored
case StatusWontfix:
// Only count wontfix if it's not justified with valid reasons
return !s.isJustifiedWontfix(finding)
default:
return true
}
}
// isJustifiedWontfix checks if wontfix has valid justification
func (s *Scorer) isJustifiedWontfix(finding Finding) bool {
if finding.Metadata == nil {
return false
}
note, exists := finding.Metadata["resolution_note"]
if !exists {
return false
}
// Valid wontfix justifications
validJustifications := []string{
"legacy", "deprecated", "external", "third-party",
"temporary", "placeholder", "documentation",
"test-only", "example", "sample",
}
note = strings.ToLower(note)
for _, justification := range validJustifications {
if strings.Contains(note, justification) {
return true
}
}
return false
}
// getStrictMultiplier returns severity multiplier for strict scoring
func (s *Scorer) getStrictMultiplier(finding Finding) int {
switch finding.Severity {
case SeverityT1:
return 1 // T1 issues are less critical
case SeverityT2:
return 2 // T2 issues are moderately important
case SeverityT3:
return 3 // T3 issues need attention
case SeverityT4:
return 5 // T4 issues are critical
default:
return 1
}
}
// 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 {
// Strict percentage calculation with multiple factors
if score <= 0 {
return 100
}
// Base calculation with stricter normalization
var percentage int
if score > 10000 {
// Logarithmic scaling for very high scores
percentage = 100 - int(float64(score-10000)/float64(score)*90)
} else if score > 5000 {
// Linear scaling for high scores
percentage = 100 - (score * 100 / 20000)
} else if score > 1000 {
// Linear scaling for medium scores
percentage = 100 - (score * 100 / 10000)
} else {
// Linear scaling for low scores
percentage = 100 - (score * 100 / 2000)
}
if percentage < 0 {
percentage = 0
}
return percentage
}
// GetStrictHealthMetrics returns comprehensive strict health metrics
func (s *Scorer) GetStrictHealthMetrics(findings []Finding) map[string]interface{} {
total := len(findings)
open := 0
critical := 0
high := 0
medium := 0
low := 0
resolved := 0
ignored := 0
strictScore := 0
totalScore := 0
for _, finding := range findings {
totalScore += finding.Score * int(finding.Severity)
switch finding.Status {
case StatusOpen:
open++
if s.isStrictlyRelevant(finding) {
strictScore += finding.Score * int(finding.Severity) * s.getStrictMultiplier(finding)
}
case StatusFixed:
resolved++
case StatusIgnored, StatusFalsePositive:
ignored++
}
switch finding.Severity {
case SeverityT4:
critical++
case SeverityT3:
high++
case SeverityT2:
medium++
case SeverityT1:
low++
}
}
// Calculate strict percentages
openPercentage := float64(open) / float64(total) * 100
criticalPercentage := float64(critical) / float64(total) * 100
resolutionRate := float64(resolved) / float64(total) * 100
// Strict health score (0-100)
healthScore := 100.0
healthScore -= float64(openPercentage) * 0.5 // Penalty for open issues
healthScore -= float64(criticalPercentage) * 2.0 // Higher penalty for critical
healthScore -= float64(high) * 0.1 // Penalty for high severity
healthScore += float64(resolutionRate) * 0.3 // Bonus for resolution rate
if healthScore < 0 {
healthScore = 0
}
if healthScore > 100 {
healthScore = 100
}
return map[string]interface{}{
"total_issues": total,
"open_issues": open,
"critical_issues": critical,
"high_issues": high,
"medium_issues": medium,
"low_issues": low,
"resolved_issues": resolved,
"ignored_issues": ignored,
"open_percentage": openPercentage,
"critical_percentage": criticalPercentage,
"resolution_rate": resolutionRate,
"strict_score": strictScore,
"total_score": totalScore,
"health_score": healthScore,
"grade": s.GetStrictGrade(healthScore),
}
}
// GetStrictGrade returns grade based on strict health score
func (s *Scorer) GetStrictGrade(healthScore float64) string {
switch {
case healthScore >= 95:
return "A+"
case healthScore >= 90:
return "A"
case healthScore >= 85:
return "A-"
case healthScore >= 80:
return "B+"
case healthScore >= 75:
return "B"
case healthScore >= 70:
return "B-"
case healthScore >= 65:
return "C+"
case healthScore >= 60:
return "C"
case healthScore >= 55:
return "C-"
case healthScore >= 50:
return "D+"
case healthScore >= 45:
return "D"
case healthScore >= 40:
return "D-"
default:
return "F"
}
}
// 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(`
🔍 STRICT 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 += "\n🚨 Findings 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 {
emoji := s.getSeverityEmoji(severity)
output += fmt.Sprintf(" %s %s: %d\n", emoji, name, count)
}
}
output += "\n📋 Status Breakdown:\n"
for status, count := range card.StatusByType {
output += fmt.Sprintf(" - %s: %d\n", status, count)
}
output += fmt.Sprintf("\n⏰ Last Scan: %s\n", card.LastScan.Format("2006-01-02 15:04:05"))
return output
}
// getSeverityEmoji returns emoji for severity level
func (s *Scorer) getSeverityEmoji(severity Severity) string {
switch severity {
case SeverityT1:
return "🟢"
case SeverityT2:
return "🟡"
case SeverityT3:
return "🟠"
case SeverityT4:
return "🔴"
default:
return "⚪"
}
}
// FormatStrictScorecard formats comprehensive strict scorecard
func (s *Scorer) FormatStrictScorecard(findings []Finding, lastScan time.Time) string {
metrics := s.GetStrictHealthMetrics(findings)
output := fmt.Sprintf(`
🔬 COMPREHENSIVE STRICT ANALYSIS
=======================================
🎯 STRICT HEALTH SCORE: %.1f/100 (%s)
=======================================
📊 ISSUE BREAKDOWN:
Total Issues: %v
🔴 Critical (T4): %v (%.1f%%)
🟠 High (T3): %v
🟡 Medium (T2): %v
🟢 Low (T1): %v
📈 STATUS ANALYSIS:
✅ Resolved: %v (%.1f%%)
🔓 Open: %v (%.1f%%)
⏸️ Ignored: %v
⚖️ SCORING:
Strict Score: %v
Total Score: %v
Health Multiplier: %.2fx
🎯 STRICT CRITERIA:
✓ Only unresolved issues counted
✓ Severity-weighted scoring (T1×1, T2×2, T3×3, T4×5)
✓ Justified wontfix excluded
✓ False positives ignored
✓ Resolution rate bonus applied
📅 Last Analysis: %s
🏆 RECOMMENDATIONS:
`,
metrics["health_score"], metrics["grade"],
metrics["total_issues"],
metrics["critical_issues"], metrics["critical_percentage"],
metrics["high_issues"],
metrics["medium_issues"],
metrics["low_issues"],
metrics["resolved_issues"], metrics["resolution_rate"],
metrics["open_issues"], metrics["open_percentage"],
metrics["ignored_issues"],
metrics["strict_score"], metrics["total_score"],
float64(metrics["strict_score"].(int))/float64(metrics["total_score"].(int)),
lastScan.Format("2006-01-02 15:04:05"))
// Add recommendations based on metrics
if metrics["critical_percentage"].(float64) > 5 {
output += " 🚨 CRITICAL: Address T4 issues immediately\n"
}
if metrics["open_percentage"].(float64) > 70 {
output += " 📈 HIGH DEBT: Focus on resolving open issues\n"
}
if metrics["resolution_rate"].(float64) < 20 {
output += " ⚡ LOW RESOLUTION: Increase fix rate\n"
}
if healthScore, ok := metrics["health_score"].(float64); ok && healthScore < 50 {
output += " ❌ POOR HEALTH: Major refactoring needed\n"
} else if healthScore >= 80 {
output += " ✅ GOOD HEALTH: Maintain current practices\n"
}
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,
}
}