mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user