mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
i dont like commits
This commit is contained in:
+11
-725
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user