|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "flag" |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "os" |
| 9 | + "os/exec" |
| 10 | + "strings" |
| 11 | + |
| 12 | + "github.com/gosuda/JustGoodPractices/internal/analyzer" |
| 13 | + |
| 14 | + "golang.org/x/tools/go/analysis/singlechecker" |
| 15 | +) |
| 16 | + |
| 17 | +type Priority string |
| 18 | + |
| 19 | +const ( |
| 20 | + P0 Priority = "P0_BLOCKER" |
| 21 | + P1 Priority = "P1_HIGH" |
| 22 | + P2 Priority = "P2_MEDIUM" |
| 23 | + P3 Priority = "P3_INFO" |
| 24 | +) |
| 25 | + |
| 26 | +type AgentRule struct { |
| 27 | + ID string `json:"id"` |
| 28 | + Title string `json:"title"` |
| 29 | + SourceFile string `json:"source_file"` |
| 30 | + SourceURL string `json:"source_url"` |
| 31 | + PublishedDate string `json:"published_date"` |
| 32 | + LanguageScope string `json:"language_scope"` |
| 33 | + CategoryID string `json:"category_id"` |
| 34 | + Priority Priority `json:"priority"` |
| 35 | + Summary string `json:"summary"` |
| 36 | + Rationale string `json:"rationale"` |
| 37 | + EnforcingPrompt string `json:"enforcing_prompt"` |
| 38 | + Dos []string `json:"dos"` |
| 39 | + Donts []string `json:"donts"` |
| 40 | + LinterRuleID string `json:"linter_rule_id,omitempty"` |
| 41 | +} |
| 42 | + |
| 43 | +type Registry struct { |
| 44 | + Version string `json:"version"` |
| 45 | + TotalRules int `json:"total_rules"` |
| 46 | + GeneratedAt string `json:"generated_at"` |
| 47 | + PriorityCounts map[string]int `json:"priority_counts"` |
| 48 | + Rules []AgentRule `json:"rules"` |
| 49 | +} |
| 50 | + |
| 51 | +func loadRegistry() (*Registry, error) { |
| 52 | + data, err := os.ReadFile("registry/rules_registry.json") |
| 53 | + if err != nil { |
| 54 | + return nil, fmt.Errorf("failed to read registry: %w", err) |
| 55 | + } |
| 56 | + var reg Registry |
| 57 | + if err := json.Unmarshal(data, ®); err != nil { |
| 58 | + return nil, fmt.Errorf("failed to parse registry: %w", err) |
| 59 | + } |
| 60 | + return ®, nil |
| 61 | +} |
| 62 | + |
| 63 | +func main() { |
| 64 | + if len(os.Args) < 2 { |
| 65 | + printUsage() |
| 66 | + os.Exit(1) |
| 67 | + } |
| 68 | + |
| 69 | + command := os.Args[1] |
| 70 | + |
| 71 | + switch command { |
| 72 | + case "lint": |
| 73 | + handleLint() |
| 74 | + case "check": |
| 75 | + os.Exit(handleCheck()) |
| 76 | + case "prompt": |
| 77 | + handlePrompt() |
| 78 | + case "list": |
| 79 | + handleList() |
| 80 | + case "sync": |
| 81 | + handleSync() |
| 82 | + case "help", "-h", "--help": |
| 83 | + printUsage() |
| 84 | + default: |
| 85 | + // Default to singlechecker passing remaining args |
| 86 | + handleLint() |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | +func printUsage() { |
| 91 | + fmt.Println(`gojgp - Go engineering practice linter |
| 92 | +
|
| 93 | +Usage: |
| 94 | + gojgp <command> [arguments] |
| 95 | +
|
| 96 | +Commands: |
| 97 | + lint [flags] [packages] Run the Go AST analyzer |
| 98 | + check Run tests and the analyzer in the current module |
| 99 | + prompt [flags] Generate rule instructions |
| 100 | + list List cataloged rules |
| 101 | + sync Refresh the rule registry |
| 102 | +
|
| 103 | +Examples: |
| 104 | + gojgp lint ./... |
| 105 | + gojgp check |
| 106 | + gojgp prompt --priority=P0`) |
| 107 | +} |
| 108 | + |
| 109 | +func handleLint() { |
| 110 | + // Shift os.Args so singlechecker receives standard package arguments |
| 111 | + os.Args = append([]string{os.Args[0]}, os.Args[2:]...) |
| 112 | + singlechecker.Main(analyzer.Analyzer) |
| 113 | +} |
| 114 | + |
| 115 | +type commandRunner func(stdout, stderr io.Writer, name string, args ...string) error |
| 116 | + |
| 117 | +func handleCheck() int { |
| 118 | + return runCheck(os.Stdout, os.Stderr, executeCommand) |
| 119 | +} |
| 120 | + |
| 121 | +func runCheck(stdout, stderr io.Writer, run commandRunner) int { |
| 122 | + status := 0 |
| 123 | + |
| 124 | + fmt.Fprintln(stdout, "=== [gojgp] 1. Running unit tests ===") |
| 125 | + if err := run(stdout, stderr, "go", "test", "./..."); err != nil { |
| 126 | + fmt.Fprintf(stderr, "FAIL: go test: %v\n", err) |
| 127 | + status = 1 |
| 128 | + } else { |
| 129 | + fmt.Fprintln(stdout, "PASS: unit tests") |
| 130 | + } |
| 131 | + |
| 132 | + fmt.Fprintln(stdout, "\n=== [gojgp] 2. Running Go AST linter ===") |
| 133 | + if err := run(stdout, stderr, os.Args[0], "lint", "./..."); err != nil { |
| 134 | + fmt.Fprintf(stderr, "FAIL: gojgp lint: %v\n", err) |
| 135 | + status = 1 |
| 136 | + } else { |
| 137 | + fmt.Fprintln(stdout, "PASS: gojgp lint") |
| 138 | + } |
| 139 | + |
| 140 | + return status |
| 141 | +} |
| 142 | + |
| 143 | +func executeCommand(stdout, stderr io.Writer, name string, args ...string) error { |
| 144 | + command := exec.Command(name, args...) |
| 145 | + command.Stdout = stdout |
| 146 | + command.Stderr = stderr |
| 147 | + return command.Run() |
| 148 | +} |
| 149 | + |
| 150 | +func handlePrompt() { |
| 151 | + fs := flag.NewFlagSet("prompt", flag.ExitOnError) |
| 152 | + catFlag := fs.String("category", "", "Filter by category ID (e.g. control_flow, test_doubles)") |
| 153 | + priFlag := fs.String("priority", "", "Filter by minimum priority (e.g. P0, P1)") |
| 154 | + scopeFlag := fs.String("scope", "", "Filter by language scope (e.g. go, python, universal)") |
| 155 | + fs.Parse(os.Args[2:]) |
| 156 | + |
| 157 | + reg, err := loadRegistry() |
| 158 | + if err != nil { |
| 159 | + fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err) |
| 160 | + os.Exit(1) |
| 161 | + } |
| 162 | + |
| 163 | + fmt.Println("### AGENT ENFORCING INSTRUCTIONS (gojgp: Go Just Good Practices) ###") |
| 164 | + fmt.Println() |
| 165 | + fmt.Println("When writing, modifying, or reviewing code, strictly comply with the following golden rules:") |
| 166 | + fmt.Println() |
| 167 | + |
| 168 | + count := 0 |
| 169 | + for _, rule := range reg.Rules { |
| 170 | + if rule.Priority == P3 { |
| 171 | + continue |
| 172 | + } |
| 173 | + if *catFlag != "" && rule.CategoryID != *catFlag { |
| 174 | + continue |
| 175 | + } |
| 176 | + if *priFlag != "" && !matchesPriority(rule.Priority, *priFlag) { |
| 177 | + continue |
| 178 | + } |
| 179 | + if *scopeFlag != "" && rule.LanguageScope != *scopeFlag && rule.LanguageScope != "universal" { |
| 180 | + continue |
| 181 | + } |
| 182 | + |
| 183 | + count++ |
| 184 | + fmt.Printf("#### [%s] %s\n", rule.Priority, rule.Title) |
| 185 | + fmt.Printf("**Rule**: %s\n", rule.Summary) |
| 186 | + fmt.Printf("**Instruction**: %s\n", rule.EnforcingPrompt) |
| 187 | + if len(rule.Dos) > 0 { |
| 188 | + fmt.Printf("- **DO**: %s\n", strings.Join(rule.Dos, "; ")) |
| 189 | + } |
| 190 | + if len(rule.Donts) > 0 { |
| 191 | + fmt.Printf("- **DON'T**: %s\n", strings.Join(rule.Donts, "; ")) |
| 192 | + } |
| 193 | + fmt.Println() |
| 194 | + } |
| 195 | + |
| 196 | + if count == 0 { |
| 197 | + fmt.Println("No matching rules found for specified filter.") |
| 198 | + } |
| 199 | +} |
| 200 | + |
| 201 | +func matchesPriority(p Priority, filter string) bool { |
| 202 | + filter = strings.ToUpper(filter) |
| 203 | + switch filter { |
| 204 | + case "P0": |
| 205 | + return p == P0 |
| 206 | + case "P1": |
| 207 | + return p == P0 || p == P1 |
| 208 | + case "P2": |
| 209 | + return p == P0 || p == P1 || p == P2 |
| 210 | + default: |
| 211 | + return string(p) == filter |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +func handleList() { |
| 216 | + reg, err := loadRegistry() |
| 217 | + if err != nil { |
| 218 | + fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err) |
| 219 | + os.Exit(1) |
| 220 | + } |
| 221 | + |
| 222 | + fmt.Printf("Total Rules Cataloged in gojgp: %d\n\n", reg.TotalRules) |
| 223 | + |
| 224 | + currentPriority := Priority("") |
| 225 | + for _, rule := range reg.Rules { |
| 226 | + if rule.Priority != currentPriority { |
| 227 | + currentPriority = rule.Priority |
| 228 | + fmt.Printf("\n[%s]\n", currentPriority) |
| 229 | + fmt.Println(strings.Repeat("-", 60)) |
| 230 | + } |
| 231 | + fmt.Printf(" • %-50s [%s] (%s)\n", rule.Title, rule.CategoryID, rule.LanguageScope) |
| 232 | + } |
| 233 | +} |
| 234 | + |
| 235 | +func handleSync() { |
| 236 | + fmt.Println("Running Rule Engine to sync database...") |
| 237 | + cmd := exec.Command("go", "run", "cmd/rule-engine/main.go") |
| 238 | + cmd.Stdout = os.Stdout |
| 239 | + cmd.Stderr = os.Stderr |
| 240 | + if err := cmd.Run(); err != nil { |
| 241 | + fmt.Fprintf(os.Stderr, "Rule engine sync failed: %v\n", err) |
| 242 | + os.Exit(1) |
| 243 | + } |
| 244 | + fmt.Println("✓ Rules database synced successfully.") |
| 245 | +} |
0 commit comments