i dont like commits

This commit is contained in:
Tomas Dvorak
2026-02-24 12:10:13 +01:00
parent 898a3c303f
commit 1d72a1cc01
109 changed files with 43586 additions and 8484 deletions
+11 -725
View File
@@ -1,734 +1,20 @@
package cmd
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"
import "github.com/spf13/cobra"
"github.com/spf13/cobra"
"github.com/yourorg/devour/internal/quality"
"github.com/yourorg/devour/internal/quality/detectors"
"github.com/yourorg/devour/internal/quality/plugins"
"github.com/yourorg/devour/internal/quality/plugins/go/fixers"
"github.com/yourorg/devour/internal/quality/review"
_ "github.com/yourorg/devour/internal/quality/plugins/go"
)
// qualityCmd represents the quality command
var qualityCmd = &cobra.Command{
Use: "quality",
Short: "Code quality analysis and technical debt tracking",
Long: `Analyze code quality issues like complexity, duplication, naming inconsistencies,
and more. Supports multiple languages including Go, Python, TypeScript, Java, and Rust.
Examples:
devour quality scan ./src # Scan current directory
devour quality scan --lang go ./src # Scan with explicit language
devour quality status # Show current status
devour quality next # Show next priority issue
devour quality resolve fixed 123 --note "Refactored" # Mark issue as fixed`,
}
// scanCmd represents the scan subcommand
var scanCmd = &cobra.Command{
Use: "scan [path]",
Short: "Run code quality analysis",
Long: `Scan the given path for code quality issues. Automatically detects language
unless specified with --lang flag.
The scan will detect:
- Complexity issues (nested loops, excessive function calls)
- Code duplication and near-duplicates
- Naming inconsistencies across directories
- Large files and god classes
- Orphaned code and unused exports`,
RunE: runQualityScan,
}
// qualityStatusCmd represents the quality status subcommand
var qualityStatusCmd = &cobra.Command{
Use: "status",
Short: "Show code quality status and scorecard",
Long: `Display the current code quality status including:
- Overall health score and grade
- Findings broken down by type and severity
- Progress metrics and next steps`,
RunE: runQualityStatus,
}
// nextCmd represents the next subcommand
var nextCmd = &cobra.Command{
Use: "next",
Short: "Show the next highest priority issue to fix",
Long: `Display the next highest priority finding based on severity and score.
This helps you focus on the most impactful improvements first.`,
RunE: runQualityNext,
}
// resolveCmd represents the resolve subcommand
var resolveCmd = &cobra.Command{
Use: "resolve <status> <id>",
Short: "Mark a finding as resolved",
Long: `Mark a finding with a specific status:
- fixed: Issue has been resolved
- wontfix: Issue won't be fixed (valid reason required)
- false_positive: Finding is incorrect
- ignore: Temporarily ignore the finding
Examples:
devour quality resolve fixed abc123 --note "Refactored complex function"
devour quality resolve wontfix def456 --note "Legacy code, planned replacement"`,
RunE: runQualityResolve,
}
// Quality flags
var (
qualityPath string
qualityLanguage string
qualityExclude []string
qualityThreshold int
qualityMinLOC int
qualityTargetScore int
qualityFormat string
qualityResetSubjective bool
qualityNoBadge bool
qualityBadgePath string
)
var explain bool
var tier int
var resolveNote string
var attest string
var statusNarrative bool
var fixDryRun bool
var fixAll bool
var reviewPrepare bool
var reviewImport string
var fixCmd = &cobra.Command{
Use: "fix [id]",
Short: "Auto-fix code quality issues",
Long: `Automatically fix T1 (auto-fixable) issues.
Examples:
devour quality fix unused_import::file.go::fmt # Fix specific issue
devour quality fix --all --dry-run # Preview all fixes
devour quality fix --all # Fix all auto-fixable issues`,
RunE: runQualityFix,
}
var reviewCmd = &cobra.Command{
Use: "review",
Short: "Generate or import AI review packets",
Long: `Generate a review packet for AI analysis, or import responses.
Examples:
devour quality review --prepare # Generate review packet
devour quality review --import responses.json # Import AI responses`,
RunE: runQualityReview,
Use: "quality [desloppify-args...]",
Short: "Code quality workflows powered by desloppify",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
forward := args
if len(forward) == 0 {
forward = []string{"--help"}
}
return runDesloppifyFromCommand(cmd, forward, true)
},
}
func init() {
rootCmd.AddCommand(qualityCmd)
qualityCmd.AddCommand(scanCmd, qualityStatusCmd, nextCmd, resolveCmd, fixCmd, reviewCmd)
// Scan flags
scanCmd.Flags().StringVar(&qualityPath, "path", ".", "Path to scan")
scanCmd.Flags().StringVar(&qualityLanguage, "lang", "", "Language (auto-detected if not specified)")
scanCmd.Flags().StringSliceVar(&qualityExclude, "exclude", []string{}, "Exclude patterns")
scanCmd.Flags().IntVar(&qualityThreshold, "threshold", 15, "Minimum score to flag an issue")
scanCmd.Flags().IntVar(&qualityMinLOC, "min-loc", 50, "Minimum lines of code to analyze")
scanCmd.Flags().IntVar(&qualityTargetScore, "target-score", 95, "Target health score")
scanCmd.Flags().StringVar(&qualityFormat, "format", "text", "Output format (text, json, strict)")
scanCmd.Flags().BoolVar(&qualityResetSubjective, "reset-subjective", false, "Reset subjective baseline")
scanCmd.Flags().BoolVar(&qualityNoBadge, "no-badge", false, "Skip badge generation")
scanCmd.Flags().StringVar(&qualityBadgePath, "badge-path", "scorecard.png", "Badge output path")
// Status flags
qualityStatusCmd.Flags().StringVar(&qualityFormat, "format", "text", "Output format (text, json, strict)")
qualityStatusCmd.Flags().BoolVar(&statusNarrative, "narrative", false, "Include narrative analysis")
// Next flags
nextCmd.Flags().BoolVar(&explain, "explain", false, "Show detailed explanation")
nextCmd.Flags().IntVar(&tier, "tier", 0, "Filter by severity tier (1-4)")
// Resolve flags
resolveCmd.Flags().StringVar(&resolveNote, "note", "", "Note explaining the resolution (required)")
resolveCmd.Flags().StringVar(&attest, "attest", "", "Attestation of improvement")
// Fix flags
fixCmd.Flags().BoolVar(&fixDryRun, "dry-run", false, "Show what would be fixed without making changes")
fixCmd.Flags().BoolVar(&fixAll, "all", false, "Fix all auto-fixable issues")
// Review flags
reviewCmd.Flags().BoolVar(&reviewPrepare, "prepare", false, "Generate review packet for AI analysis")
reviewCmd.Flags().StringVar(&reviewImport, "import", "", "Import review responses from file")
}
func runQualityScan(cmd *cobra.Command, args []string) error {
path := qualityPath
if len(args) > 0 {
path = args[0]
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return fmt.Errorf("path does not exist: %s", path)
}
config := &quality.Config{
Path: path,
Language: qualityLanguage,
Exclude: qualityExclude,
Threshold: qualityThreshold,
MinLOC: qualityMinLOC,
TargetScore: qualityTargetScore,
ResetSubjective: qualityResetSubjective,
NoBadge: qualityNoBadge,
BadgePath: qualityBadgePath,
}
scanner := quality.NewScanner(config)
finder := quality.NewDefaultFileFinder()
scanner.SetFileFinder(finder)
lang := qualityLanguage
if lang == "" {
lang = quality.DetectLanguage(path)
fmt.Printf("Auto-detected language: %s\n", lang)
}
plugin, ok := plugins.Get(lang)
if ok {
fmt.Printf("Using %s plugin with AST analysis\n", lang)
for _, detector := range plugin.CreateDetectors(finder) {
scanner.RegisterDetector(detector)
}
} else {
scanner.RegisterDetector(detectors.NewComplexityDetector(finder))
scanner.RegisterDetector(detectors.NewDuplicationDetector(finder))
scanner.RegisterDetector(detectors.NewNamingDetector(finder))
}
ctx := context.Background()
result, err := scanner.Scan(ctx)
if err != nil {
return fmt.Errorf("scan failed: %w", err)
}
result.Findings = quality.AttachDocsEvidence(lang, result.Findings)
return outputScanResult(result, qualityFormat)
}
func runQualityStatus(cmd *cobra.Command, args []string) error {
// Load previous scan results
dataDir := filepath.Join(".", "devour_data", "quality")
statusFile := filepath.Join(dataDir, "status.json")
var findings []quality.Finding
var lastScan time.Time
if data, err := os.ReadFile(statusFile); err == nil {
var status struct {
Findings []quality.Finding `json:"findings"`
Timestamp time.Time `json:"timestamp"`
}
if err := json.Unmarshal(data, &status); err == nil {
findings = status.Findings
lastScan = status.Timestamp
}
}
if len(findings) == 0 {
fmt.Println("No previous scan results found. Run 'devour quality scan' first.")
return nil
}
// Generate scorecard
scorer := quality.NewScorer(qualityTargetScore)
scorecard := scorer.GenerateScorecard(findings, lastScan)
// Output based on format
switch qualityFormat {
case "json":
return json.NewEncoder(os.Stdout).Encode(scorecard)
case "strict":
fmt.Println(scorer.FormatStrictScorecard(findings, lastScan))
printQualityEvidenceSummary(findings)
return nil
default:
fmt.Println(scorer.FormatScorecard(scorecard))
printQualityEvidenceSummary(findings)
return nil
}
}
func runQualityNext(cmd *cobra.Command, args []string) error {
// Load previous scan results
dataDir := filepath.Join(".", "devour_data", "quality")
statusFile := filepath.Join(dataDir, "status.json")
var findings []quality.Finding
if data, err := os.ReadFile(statusFile); err == nil {
var status struct {
Findings []quality.Finding `json:"findings"`
}
if err := json.Unmarshal(data, &status); err == nil {
findings = status.Findings
}
}
if len(findings) == 0 {
fmt.Println("No findings found. Run 'devour quality scan' first.")
return nil
}
// Get next priority finding
scorer := quality.NewScorer(qualityTargetScore)
next := scorer.GetNextPriority(findings)
if next == nil {
fmt.Println("🎉 No open issues to fix!")
return nil
}
// Filter by tier if specified
if tier > 0 {
if int(next.Severity) != tier {
// Find next in specified tier
for _, finding := range findings {
if finding.Status == quality.StatusOpen && int(finding.Severity) == tier {
next = &finding
break
}
}
if next == nil || int(next.Severity) != tier {
fmt.Printf("No open issues found in tier %d.\n", tier)
return nil
}
}
}
// Display finding
fmt.Printf("Next Priority Issue (T%d)\n", int(next.Severity))
fmt.Println("=======================================")
fmt.Printf("File: %s:%d\n", next.File, next.Line)
fmt.Printf("Title: %s\n", next.Title)
fmt.Printf("Score: %d\n", next.Score)
fmt.Printf("ID: %s\n", next.ID)
fmt.Printf("\nDescription:\n%s\n", next.Description)
if next.Metadata != nil {
if urls := strings.TrimSpace(next.Metadata["docs_evidence_urls"]); urls != "" {
fmt.Printf("\nEvidence Docs:\n%s\n", urls)
}
if rationale := strings.TrimSpace(next.Metadata["docs_evidence_rationale"]); rationale != "" {
fmt.Printf("\nRationale:\n%s\n", rationale)
}
if confidence := strings.TrimSpace(next.Metadata["docs_evidence_confidence"]); confidence != "" {
fmt.Printf("Evidence confidence: %s\n", confidence)
}
}
if explain {
fmt.Printf("\nExplanation:\n")
fmt.Printf("This is a T%d severity issue. ", int(next.Severity))
switch next.Severity {
case quality.SeverityT1:
fmt.Println("T1 issues are typically auto-fixable like unused imports or debug logs.")
case quality.SeverityT2:
fmt.Println("T2 issues require quick manual fixes like unused variables or dead exports.")
case quality.SeverityT3:
fmt.Println("T3 issues need judgment calls like near-duplicates or single-use abstractions.")
case quality.SeverityT4:
fmt.Println("T4 issues require major refactoring like god components or mixed concerns.")
}
fmt.Printf("\nTo fix: devour quality resolve fixed %s --note \"Describe what you fixed\"\n", next.ID)
}
return nil
}
func runQualityResolve(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: devour quality resolve <status> <id>")
}
status := quality.Status(args[0])
id := args[1]
// Validate status
validStatuses := map[quality.Status]bool{
quality.StatusFixed: true,
quality.StatusWontfix: true,
quality.StatusFalsePositive: true,
quality.StatusIgnored: true,
}
if !validStatuses[status] {
return fmt.Errorf("invalid status: %s", status)
}
if resolveNote == "" {
return fmt.Errorf("--note is required when resolving findings")
}
// Load current findings
dataDir := filepath.Join(".", "devour_data", "quality")
statusFile := filepath.Join(dataDir, "status.json")
var findings []quality.Finding
if data, err := os.ReadFile(statusFile); err == nil {
var statusData struct {
Findings []quality.Finding `json:"findings"`
}
if err := json.Unmarshal(data, &statusData); err == nil {
findings = statusData.Findings
}
}
// Find and update the finding
found := false
for i, finding := range findings {
if finding.ID == id {
findings[i].Status = status
findings[i].UpdatedAt = time.Now()
if finding.Metadata == nil {
findings[i].Metadata = make(map[string]string)
}
findings[i].Metadata["resolution_note"] = resolveNote
if attest != "" {
findings[i].Metadata["attestation"] = attest
}
found = true
break
}
}
if !found {
return fmt.Errorf("finding not found: %s", id)
}
// Save updated findings
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
statusData := struct {
Findings []quality.Finding `json:"findings"`
Timestamp time.Time `json:"timestamp"`
}{
Findings: findings,
Timestamp: time.Now(),
}
data, err := json.MarshalIndent(statusData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal status: %w", err)
}
if err := os.WriteFile(statusFile, data, 0644); err != nil {
return fmt.Errorf("failed to save status: %w", err)
}
fmt.Printf("Resolved: %s as %s\n", id, status)
if resolveNote != "" {
fmt.Printf("Note: %s\n", resolveNote)
}
return nil
}
func outputScanResult(result *quality.ScanResult, format string) error {
// Save results to data directory
dataDir := filepath.Join(".", "devour_data", "quality")
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
statusFile := filepath.Join(dataDir, "status.json")
statusData := struct {
Findings []quality.Finding `json:"findings"`
Timestamp time.Time `json:"timestamp"`
}{
Findings: result.Findings,
Timestamp: result.Timestamp,
}
data, err := json.MarshalIndent(statusData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal results: %w", err)
}
if err := os.WriteFile(statusFile, data, 0644); err != nil {
return fmt.Errorf("failed to save results: %w", err)
}
// Note: Scorecard generation is now handled by the dedicated 'devour scorecard' command
if !qualityNoBadge && qualityBadgePath != "" {
fmt.Printf("💡 Use 'devour scorecard' to generate beautiful scorecard banners\n")
}
// Output based on format
switch format {
case "json":
return json.NewEncoder(os.Stdout).Encode(result)
default:
return formatScanResultText(result)
}
}
func formatScanResultText(result *quality.ScanResult) error {
fmt.Println("Code Quality Scan Results")
fmt.Println("=======================================")
fmt.Printf("Files checked: %d\n", result.FilesChecked)
fmt.Printf("Duration: %s\n", result.Duration)
fmt.Printf("Score: %d (strict: %d)\n", result.Score, result.StrictScore)
fmt.Printf("Findings: %d\n\n", len(result.Findings))
if len(result.Findings) == 0 {
fmt.Println("No code quality issues found.")
return nil
}
bySeverity := make(map[quality.Severity][]quality.Finding)
for _, finding := range result.Findings {
bySeverity[finding.Severity] = append(bySeverity[finding.Severity], finding)
}
tiers := []quality.Severity{quality.SeverityT4, quality.SeverityT3, quality.SeverityT2, quality.SeverityT1}
tierNames := map[quality.Severity]string{
quality.SeverityT1: "T1 (Auto-fixable)",
quality.SeverityT2: "T2 (Quick manual)",
quality.SeverityT3: "T3 (Needs judgment)",
quality.SeverityT4: "T4 (Major refactor)",
}
for _, severity := range tiers {
findings := bySeverity[severity]
if len(findings) == 0 {
continue
}
fmt.Printf("[%s] %d issues\n", tierNames[severity], len(findings))
for _, finding := range findings {
fmt.Printf(" - %s:%d - %s (score: %d)\n",
filepath.Base(finding.File), finding.Line, finding.Title, finding.Score)
}
fmt.Println()
}
fmt.Println("Run 'devour quality next' to see the highest priority issue to fix.")
fmt.Println("Run 'devour quality status' for detailed scorecard.")
return nil
}
func runQualityFix(cmd *cobra.Command, args []string) error {
dataDir := filepath.Join(".", "devour_data", "quality")
statusFile := filepath.Join(dataDir, "status.json")
var findings []quality.Finding
if data, err := os.ReadFile(statusFile); err == nil {
var status struct {
Findings []quality.Finding `json:"findings"`
}
if err := json.Unmarshal(data, &status); err == nil {
findings = status.Findings
}
}
if len(findings) == 0 {
fmt.Println("No findings found. Run 'devour quality scan' first.")
return nil
}
availableFixers := []plugins.Fixer{
fixers.NewUnusedImportFixer(),
fixers.NewFormattingFixer(),
}
ctx := context.Background()
fixed := 0
var errors []string
if fixAll {
for _, finding := range findings {
if finding.Status != quality.StatusOpen || finding.Severity != quality.SeverityT1 {
continue
}
for _, fixer := range availableFixers {
if fixer.CanFix(finding) {
result, err := fixer.Fix(ctx, finding, fixDryRun)
if err != nil {
errors = append(errors, fmt.Sprintf("%s: %v", finding.ID, err))
continue
}
if result.Success {
fixed++
fmt.Printf("[OK] %s\n", result.Message)
}
break
}
}
}
} else {
if len(args) < 1 {
return fmt.Errorf("specify a finding ID or use --all")
}
targetID := args[0]
var target *quality.Finding
for i := range findings {
if findings[i].ID == targetID {
target = &findings[i]
break
}
}
if target == nil {
return fmt.Errorf("finding not found: %s", targetID)
}
for _, fixer := range availableFixers {
if fixer.CanFix(*target) {
result, err := fixer.Fix(ctx, *target, fixDryRun)
if err != nil {
return fmt.Errorf("fix failed: %w", err)
}
fmt.Printf("[OK] %s\n", result.Message)
fixed = 1
break
}
}
}
if fixDryRun {
fmt.Printf("\nDry run complete. %d issues would be fixed.\n", fixed)
} else {
fmt.Printf("\nFixed %d issues.\n", fixed)
}
if len(errors) > 0 {
fmt.Printf("\nErrors:\n")
for _, e := range errors {
fmt.Printf(" • %s\n", e)
}
}
return nil
}
func runQualityReview(cmd *cobra.Command, args []string) error {
dataDir := filepath.Join(".", "devour_data")
if reviewPrepare {
return prepareReviewPacket(dataDir)
}
if reviewImport != "" {
return importReviewResponses(dataDir, reviewImport)
}
return fmt.Errorf("use --prepare to generate a review packet or --import <file> to import responses")
}
func prepareReviewPacket(dataDir string) error {
statusFile := filepath.Join(dataDir, "quality", "status.json")
var findings []quality.Finding
var lastScan time.Time
if data, err := os.ReadFile(statusFile); err == nil {
var status struct {
Findings []quality.Finding `json:"findings"`
Timestamp time.Time `json:"timestamp"`
}
if err := json.Unmarshal(data, &status); err == nil {
findings = status.Findings
lastScan = status.Timestamp
}
}
scorer := quality.NewScorer(qualityTargetScore)
scorecard := scorer.GenerateScorecard(findings, lastScan)
gen := review.NewPacketGenerator(dataDir)
packet, err := gen.Generate(findings, scorecard, "go")
if err != nil {
return fmt.Errorf("failed to generate review packet: %w", err)
}
filename := fmt.Sprintf("review-%s.json", time.Now().Format("20060102-150405"))
if err := gen.Save(packet, filename); err != nil {
return fmt.Errorf("failed to save review packet: %w", err)
}
fmt.Printf("Review packet generated: %s/review/%s\n", dataDir, filename)
fmt.Printf("Findings to review: %d\n", len(packet.Findings))
fmt.Printf("Questions: %d\n", len(packet.Questions))
return nil
}
func importReviewResponses(dataDir string, filename string) error {
gen := review.NewPacketGenerator(dataDir)
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read responses file: %w", err)
}
var responses map[string]string
var respData struct {
Responses map[string]string `json:"responses"`
}
if err := json.Unmarshal(data, &respData); err == nil {
responses = respData.Responses
} else {
var simpleResponses map[string]string
if err := json.Unmarshal(data, &simpleResponses); err != nil {
return fmt.Errorf("failed to parse responses: %w", err)
}
responses = simpleResponses
}
if err := gen.ImportReview(filepath.Base(filename), responses); err != nil {
return fmt.Errorf("failed to import responses: %w", err)
}
fmt.Printf("Imported %d review responses\n", len(responses))
return nil
}
func printQualityEvidenceSummary(findings []quality.Finding) {
totalWithEvidence := 0
for _, f := range findings {
if f.Metadata != nil && strings.TrimSpace(f.Metadata["docs_evidence_urls"]) != "" {
totalWithEvidence++
}
}
if totalWithEvidence == 0 {
return
}
fmt.Printf("\nEvidence-linked findings: %d/%d\n", totalWithEvidence, len(findings))
for _, f := range findings {
if f.Metadata == nil {
continue
}
urls := strings.TrimSpace(f.Metadata["docs_evidence_urls"])
if urls == "" {
continue
}
fmt.Printf(" • %s:%d - %s\n %s\n", filepath.Base(f.File), f.Line, f.Title, urls)
break
}
}