mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
737 lines
21 KiB
Go
737 lines
21 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"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/scorecard"
|
|
|
|
_ "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 qualityUseAST bool
|
|
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,
|
|
}
|
|
|
|
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)")
|
|
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)")
|
|
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)
|
|
}
|
|
|
|
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)
|
|
default:
|
|
fmt.Println(scorer.FormatScorecard(scorecard))
|
|
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 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)
|
|
}
|
|
|
|
// Generate scorecard badge if not disabled
|
|
if !qualityNoBadge && qualityBadgePath != "" {
|
|
if err := generateScorecardBadge(result, qualityBadgePath); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Warning: failed to generate scorecard badge: %v\n", err)
|
|
} else {
|
|
fmt.Printf("Scorecard badge generated: %s\n", qualityBadgePath)
|
|
}
|
|
}
|
|
|
|
// Output based on format
|
|
switch format {
|
|
case "json":
|
|
return json.NewEncoder(os.Stdout).Encode(result)
|
|
default:
|
|
return formatScanResultText(result)
|
|
}
|
|
}
|
|
|
|
func generateScorecardBadge(result *quality.ScanResult, outputPath string) error {
|
|
// Calculate grade from score
|
|
scorer := quality.NewScorer(qualityTargetScore)
|
|
grade := scorer.GetHealthGrade(result.StrictScore)
|
|
|
|
// Group findings by type and tier
|
|
findByType := make(map[string]int)
|
|
findByTier := make(map[string]int)
|
|
for _, f := range result.Findings {
|
|
findByType[f.Type]++
|
|
tierName := fmt.Sprintf("T%d", int(f.Severity))
|
|
findByTier[tierName]++
|
|
}
|
|
|
|
// Get project name from current directory
|
|
projectName := "devour"
|
|
if dir, err := os.Getwd(); err == nil {
|
|
projectName = filepath.Base(dir)
|
|
}
|
|
|
|
// Prepare scorecard data
|
|
scoreData := &scorecard.ScorecardData{
|
|
ProjectName: projectName,
|
|
Version: "", // Could be extracted from version info
|
|
OverallScore: float64(result.Score),
|
|
StrictScore: float64(result.StrictScore),
|
|
Grade: grade,
|
|
FindingsTotal: len(result.Findings),
|
|
FindingsOpen: len(result.Findings), // All findings are open initially
|
|
LastScan: result.Timestamp,
|
|
FindByType: findByType,
|
|
FindByTier: findByTier,
|
|
}
|
|
|
|
return scorecard.Generate(scoreData, outputPath)
|
|
}
|
|
|
|
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)
|
|
|
|
responses := make(map[string]string)
|
|
|
|
data, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read responses file: %w", err)
|
|
}
|
|
|
|
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
|
|
}
|