Files
Devour/internal/quality/narrative.go
T
Tomas Dvorak 55885a0e8f first commit
2026-02-22 10:42:17 +01:00

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"
}