mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
439 lines
11 KiB
Go
439 lines
11 KiB
Go
package quality
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
)
|
|
|
|
type NarrativeGenerator struct {
|
|
targetScore int
|
|
}
|
|
|
|
func NewNarrativeGenerator(targetScore int) *NarrativeGenerator {
|
|
if targetScore <= 0 {
|
|
targetScore = 95
|
|
}
|
|
return &NarrativeGenerator{targetScore: targetScore}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) Generate(findings []Finding, scorecard *Scorecard, history []StateSnapshot) *Narrative {
|
|
phase := g.determinePhase(findings, scorecard)
|
|
headline := g.generateHeadline(phase, scorecard)
|
|
dimensions := g.analyzeDimensions(findings)
|
|
actions := g.generateActions(findings, phase)
|
|
strategy := g.generateStrategy(findings, dimensions)
|
|
tools := g.generateTools(findings)
|
|
debt := g.analyzeDebt(findings, scorecard)
|
|
strictTarget := g.calculateStrictTarget(scorecard)
|
|
reminders := g.generateReminders(findings, history)
|
|
riskFlags := g.identifyRisks(findings, history)
|
|
|
|
return &Narrative{
|
|
Phase: phase,
|
|
Headline: headline,
|
|
Dimensions: dimensions,
|
|
Actions: actions,
|
|
Strategy: strategy,
|
|
Tools: tools,
|
|
Debt: debt,
|
|
Milestone: g.generateMilestone(phase, scorecard),
|
|
WhyNow: g.explainWhyNow(phase, findings),
|
|
RiskFlags: riskFlags,
|
|
StrictTarget: strictTarget,
|
|
Reminders: reminders,
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) determinePhase(findings []Finding, scorecard *Scorecard) string {
|
|
openCount := 0
|
|
t4Count := 0
|
|
t3Count := 0
|
|
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen {
|
|
openCount++
|
|
if f.Severity == SeverityT4 {
|
|
t4Count++
|
|
} else if f.Severity == SeverityT3 {
|
|
t3Count++
|
|
}
|
|
}
|
|
}
|
|
|
|
if openCount == 0 {
|
|
return "maintenance"
|
|
}
|
|
|
|
if t4Count > 0 {
|
|
return "critical"
|
|
}
|
|
|
|
if t3Count > 5 || openCount > 20 {
|
|
return "debt_reduction"
|
|
}
|
|
|
|
if openCount > 5 {
|
|
return "cleanup"
|
|
}
|
|
|
|
return "polish"
|
|
}
|
|
|
|
func (g *NarrativeGenerator) generateHeadline(phase string, scorecard *Scorecard) string {
|
|
switch phase {
|
|
case "maintenance":
|
|
return "Codebase is healthy! Focus on preventing new debt."
|
|
case "critical":
|
|
return fmt.Sprintf("Critical issues detected (%d strict score). Address T4 findings first.", scorecard.StrictScore)
|
|
case "debt_reduction":
|
|
return fmt.Sprintf("Significant technical debt (%d open issues). Systematic cleanup recommended.", scorecard.TotalScore)
|
|
case "cleanup":
|
|
return fmt.Sprintf("Minor issues detected (%d open). Quick wins available.", scorecard.TotalScore)
|
|
default:
|
|
return fmt.Sprintf("Codebase in good shape (%d open issues).", scorecard.TotalScore)
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) analyzeDimensions(findings []Finding) *NarrativeDimensions {
|
|
dimensionScores := make(map[Dimension][]Finding)
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen {
|
|
dim := g.classifyDimension(f)
|
|
dimensionScores[dim] = append(dimensionScores[dim], f)
|
|
}
|
|
}
|
|
|
|
var lowest []*DimensionInfo
|
|
var biggestGap []*DimensionInfo
|
|
var stagnant []*DimensionInfo
|
|
|
|
for dim, dimFindings := range dimensionScores {
|
|
info := &DimensionInfo{
|
|
Name: string(dim),
|
|
Issues: len(dimFindings),
|
|
}
|
|
|
|
impact := 0
|
|
for _, f := range dimFindings {
|
|
impact += f.Score * int(f.Severity)
|
|
}
|
|
info.Impact = float64(impact)
|
|
|
|
lowest = append(lowest, info)
|
|
}
|
|
|
|
sort.Slice(lowest, func(i, j int) bool {
|
|
return lowest[i].Impact > lowest[j].Impact
|
|
})
|
|
|
|
if len(lowest) > 5 {
|
|
lowest = lowest[:5]
|
|
}
|
|
|
|
return &NarrativeDimensions{
|
|
LowestDimensions: lowest,
|
|
BiggestGapDimensions: biggestGap,
|
|
StagnantDimensions: stagnant,
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) classifyDimension(f Finding) Dimension {
|
|
switch f.Type {
|
|
case "complexity", "complexity_ast":
|
|
return DimensionCodeQuality
|
|
case "duplication", "dupes":
|
|
return DimensionDuplication
|
|
case "dead_code", "unused_import", "unused":
|
|
return DimensionFileHealth
|
|
case "security":
|
|
return DimensionSecurity
|
|
case "naming":
|
|
return DimensionNamingQuality
|
|
case "import_cycle", "cycles":
|
|
return DimensionAbstractionFit
|
|
default:
|
|
return DimensionCodeQuality
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) generateActions(findings []Finding, phase string) []string {
|
|
var actions []string
|
|
|
|
t1AutoFixable := 0
|
|
t2Quick := 0
|
|
t3Judgment := 0
|
|
t4Major := 0
|
|
|
|
for _, f := range findings {
|
|
if f.Status != StatusOpen {
|
|
continue
|
|
}
|
|
switch f.Severity {
|
|
case SeverityT1:
|
|
t1AutoFixable++
|
|
case SeverityT2:
|
|
t2Quick++
|
|
case SeverityT3:
|
|
t3Judgment++
|
|
case SeverityT4:
|
|
t4Major++
|
|
}
|
|
}
|
|
|
|
if t4Major > 0 {
|
|
actions = append(actions, fmt.Sprintf("Address %d T4 (major refactor) issues - these require architectural changes", t4Major))
|
|
}
|
|
|
|
if t3Judgment > 0 {
|
|
actions = append(actions, fmt.Sprintf("Review %d T3 (needs judgment) issues - decide if they need fixing", t3Judgment))
|
|
}
|
|
|
|
if t1AutoFixable > 0 {
|
|
actions = append(actions, fmt.Sprintf("Run auto-fixer for %d T1 (auto-fixable) issues", t1AutoFixable))
|
|
}
|
|
|
|
if t2Quick > 0 {
|
|
actions = append(actions, fmt.Sprintf("Quick manual fixes available for %d T2 issues", t2Quick))
|
|
}
|
|
|
|
if len(actions) == 0 {
|
|
actions = append(actions, "No immediate actions required - maintain code quality")
|
|
}
|
|
|
|
return actions
|
|
}
|
|
|
|
func (g *NarrativeGenerator) generateStrategy(findings []Finding, dimensions *NarrativeDimensions) *NarrativeStrategy {
|
|
autoFixable := 0
|
|
total := 0
|
|
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen {
|
|
total++
|
|
if f.Severity == SeverityT1 {
|
|
autoFixable++
|
|
}
|
|
}
|
|
}
|
|
|
|
var recommendation string
|
|
var coverage float64
|
|
if total > 0 {
|
|
coverage = float64(autoFixable) / float64(total) * 100
|
|
}
|
|
|
|
if coverage > 50 {
|
|
recommendation = "Use auto-fixers first, then address remaining issues manually"
|
|
} else if autoFixable > 0 {
|
|
recommendation = "Start with auto-fixers for quick wins, then prioritize by impact"
|
|
} else {
|
|
recommendation = "Prioritize by severity and impact, starting with T4 issues"
|
|
}
|
|
|
|
return &NarrativeStrategy{
|
|
FixerLeverage: &FixerLeverage{
|
|
AutoFixableCount: autoFixable,
|
|
TotalCount: total,
|
|
Coverage: coverage,
|
|
Recommendation: recommendation,
|
|
},
|
|
CanParallelize: len(findings) > 3,
|
|
Hint: g.generateHint(findings),
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) generateHint(findings []Finding) string {
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen && f.Severity == SeverityT1 {
|
|
return "T1 issues can be auto-fixed with 'devour quality fix'"
|
|
}
|
|
}
|
|
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen && f.Severity == SeverityT4 {
|
|
return "T4 issues require planning - consider creating a dedicated branch"
|
|
}
|
|
}
|
|
|
|
return "Focus on one category at a time for best results"
|
|
}
|
|
|
|
func (g *NarrativeGenerator) generateTools(findings []Finding) *NarrativeTools {
|
|
fixers := []interface{}{}
|
|
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen && f.Severity == SeverityT1 {
|
|
fixers = append(fixers, map[string]string{
|
|
"name": f.Type,
|
|
"description": fmt.Sprintf("Fix %s issues", f.Type),
|
|
})
|
|
}
|
|
}
|
|
|
|
return &NarrativeTools{
|
|
Fixers: fixers,
|
|
Plan: &PlanTool{
|
|
Command: "devour quality plan",
|
|
Description: "Generate prioritized action plan",
|
|
},
|
|
Badge: &BadgeTool{
|
|
Generated: true,
|
|
InReadme: false,
|
|
Path: "scorecard.png",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) analyzeDebt(findings []Finding, scorecard *Scorecard) *NarrativeDebt {
|
|
wontfixCount := 0
|
|
for _, f := range findings {
|
|
if f.Status == StatusWontfix {
|
|
wontfixCount++
|
|
}
|
|
}
|
|
|
|
var worstDimension string
|
|
var worstGap float64
|
|
|
|
dimensionImpact := make(map[string]float64)
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen {
|
|
dim := string(g.classifyDimension(f))
|
|
dimensionImpact[dim] += float64(f.Score * int(f.Severity))
|
|
}
|
|
}
|
|
|
|
for dim, impact := range dimensionImpact {
|
|
if impact > worstGap {
|
|
worstGap = impact
|
|
worstDimension = dim
|
|
}
|
|
}
|
|
|
|
return &NarrativeDebt{
|
|
OverallGap: float64(scorecard.StrictScore),
|
|
WontfixCount: wontfixCount,
|
|
WorstDimension: worstDimension,
|
|
WorstGap: worstGap,
|
|
Trend: "stable",
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) calculateStrictTarget(scorecard *Scorecard) *StrictTarget {
|
|
gap := float64(scorecard.StrictScore) / float64(g.targetScore) * 100
|
|
|
|
var state string
|
|
var warning *string
|
|
|
|
switch {
|
|
case gap >= 100:
|
|
state = "at_target"
|
|
case gap >= 80:
|
|
state = "near_target"
|
|
case gap >= 50:
|
|
state = "in_progress"
|
|
w := "Significant gap to target - consider focused effort"
|
|
warning = &w
|
|
default:
|
|
state = "needs_work"
|
|
w := "Large gap to target - prioritize high-impact fixes"
|
|
warning = &w
|
|
}
|
|
|
|
return &StrictTarget{
|
|
Target: float64(g.targetScore),
|
|
Current: float64(scorecard.StrictScore),
|
|
Gap: gap,
|
|
State: state,
|
|
Warning: warning,
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) generateReminders(findings []Finding, history []StateSnapshot) []string {
|
|
var reminders []string
|
|
|
|
autoFixable := 0
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen && f.Severity == SeverityT1 {
|
|
autoFixable++
|
|
}
|
|
}
|
|
|
|
if autoFixable > 0 {
|
|
reminders = append(reminders, fmt.Sprintf("%d auto-fixable issues available - use 'devour quality fix'", autoFixable))
|
|
}
|
|
|
|
if len(history) > 0 {
|
|
latest := history[len(history)-1]
|
|
if latest.Findings == len(findings) {
|
|
reminders = append(reminders, "No progress since last scan - consider tackling a specific category")
|
|
}
|
|
}
|
|
|
|
return reminders
|
|
}
|
|
|
|
func (g *NarrativeGenerator) identifyRisks(findings []Finding, history []StateSnapshot) []string {
|
|
var risks []string
|
|
|
|
t4Count := 0
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen && f.Severity == SeverityT4 {
|
|
t4Count++
|
|
}
|
|
}
|
|
|
|
if t4Count > 3 {
|
|
risks = append(risks, fmt.Sprintf("High number of T4 issues (%d) indicates architectural debt", t4Count))
|
|
}
|
|
|
|
if len(history) >= 3 {
|
|
trend := 0
|
|
for i := len(history) - 3; i < len(history); i++ {
|
|
trend += history[i].Findings
|
|
}
|
|
avg := trend / 3
|
|
if len(findings) > int(float64(avg)*1.2) {
|
|
risks = append(risks, "Finding count is trending upward - debt is accumulating")
|
|
}
|
|
}
|
|
|
|
return risks
|
|
}
|
|
|
|
func (g *NarrativeGenerator) generateMilestone(phase string, scorecard *Scorecard) string {
|
|
switch phase {
|
|
case "maintenance":
|
|
return "Maintain current quality level"
|
|
case "critical":
|
|
return "Reduce T4 issues to zero"
|
|
case "debt_reduction":
|
|
return fmt.Sprintf("Reduce strict score below %d", g.targetScore)
|
|
case "cleanup":
|
|
return "Clear all T1 and T2 issues"
|
|
default:
|
|
return "Continue quality improvement"
|
|
}
|
|
}
|
|
|
|
func (g *NarrativeGenerator) explainWhyNow(phase string, findings []Finding) string {
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen && f.Severity == SeverityT4 {
|
|
return "T4 issues compound over time - addressing them early prevents architectural decay"
|
|
}
|
|
}
|
|
|
|
t1Count := 0
|
|
for _, f := range findings {
|
|
if f.Status == StatusOpen && f.Severity == SeverityT1 {
|
|
t1Count++
|
|
}
|
|
}
|
|
|
|
if t1Count > 5 {
|
|
return "Quick wins available - auto-fixers can clear low-hanging fruit in minutes"
|
|
}
|
|
|
|
return "Consistent small improvements compound into significant quality gains"
|
|
}
|