Skip to content

Commit e2fd6a0

Browse files
committed
feat: add Go engineering skills and linter
1 parent 53d01e4 commit e2fd6a0

36 files changed

Lines changed: 7490 additions & 1 deletion

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*.dll
88
*.so
99
*.dylib
10+
/bin/
1011

1112
# Test binary, built with `go test -c`
1213
*.test
@@ -27,6 +28,9 @@ go.work.sum
2728
# env file
2829
.env
2930

31+
# Generated crawler output
32+
best_practices/
33+
3034
# Editor/IDE
3135
# .idea/
3236
# .vscode/

AGENTS.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# AI Agent Guidelines (Engineering Best Practices)
2+
3+
This repository provides provider-neutral engineering practices and deterministic Go analysis. As an AI agent working in this repository or producing changes, follow these rules.
4+
5+
---
6+
7+
## 1. Top 8 Golden Rules (Always Active)
8+
9+
1. **Small & Hermetic Tests**: Keep unit tests fast (<100ms), single-process, deterministic, and isolated. **Never use `time.Sleep()`** for test synchronization.
10+
2. **Favor Real/Fakes over Mocks**: Use real objects or high-fidelity in-memory fakes. Only use mocks when unavoidable, and only verify **state-changing** method calls.
11+
3. **Flat Control Flow & Early Returns**: Minimize nesting depth. Use guard clauses and return early. Never use redundant `else` blocks after a terminal statement (`return`/`panic`/`throw`).
12+
4. **One Map Key, One Lookup**: Pay the map lookup / hashing cost exactly once. Never check existence and immediately index into the map again.
13+
5. **Positive Booleans**: Name flags and boolean functions with positive semantics (`isValid`, `isEnabled`) to avoid confusing double-negatives (`!isNotDisabled`).
14+
6. **Construct with Collaborators, Call with Work**: Pass persistent services/clients into constructors; pass per-request data into methods.
15+
7. **Let Code Speak for Itself**: Do not write comments that describe *what* the code does. Reserve comments strictly for explaining *why* (non-obvious trade-offs, external constraints).
16+
8. **Small & Focused Changes (Prefactoring)**: Refactor first to make the change easy, then add the new feature in a separate commit/PR.
17+
18+
---
19+
20+
## 2. On-Demand Skill Routing
21+
22+
When performing specialized tasks, consult the corresponding Skill documentation:
23+
24+
| Task / Context | Skill to Activate | Key Topics |
25+
| :--- | :--- | :--- |
26+
| **Writing / Fixing Tests** | [`testing-practices`](skills/testing-practices/SKILL.md) | Test sizing, Fake vs Mock, Flakiness, Narrow Assertions, SMURF |
27+
| **Refactoring & Clean Code** | [`code-health`](skills/code-health/SKILL.md) | Nesting reduction, Positive booleans, Functional core, Map lookup |
28+
| **API & Architecture Design** | [`api-design`](skills/api-design/SKILL.md) | Hard-to-misuse APIs, Collaborators vs Work, Premature DRY, Value objects |
29+
| **PR & Code Review** | [`code-review`](skills/code-review/SKILL.md) | Small PRs, Prefactoring, Commit context, Review etiquette |
30+
31+
---
32+
33+
## 3. Automated Tooling & Deterministic Enforcement (`gojgp`)
34+
35+
Whenever writing or refactoring Go code:
36+
1. Run `go run cmd/gojgp/main.go check` (or `./bin/gojgp check`) to run unit tests and verify compliance with all Best Practices.
37+
2. Run `go run cmd/gojgp/main.go lint ./...` to check specific files/packages with the native Go AST analyzer.
38+
3. Fix any reported linter violations deterministically before completing your task.
39+
40+
---
41+
42+
## 4. Metadata Registry Reference
43+
44+
For the full index of all 147 cataloged Best Practices, inspect:
45+
- [Category Definitions](registry/categories.json)
46+
- [Rules Registry Database](registry/rules_registry.json)

README.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,59 @@
1-
# JustAGoodPractice
1+
# Go Engineering Practices
2+
3+
A provider-neutral Agent Skills pack and Go AST linter for reliable Go code, tests, APIs, and reviews.
4+
5+
## Install the skill
6+
7+
Install the complete pack:
8+
9+
```bash
10+
npx skills add gosuda/JustGoodPractices
11+
```
12+
13+
Install only the primary Go skill:
14+
15+
```bash
16+
npx skills add gosuda/JustGoodPractices --skill go-engineering-practices
17+
```
18+
19+
The pack includes:
20+
21+
- `go-engineering-practices`: end-to-end Go workflow and required lint gate.
22+
- `testing-practices`: deterministic tests, fakes, and synchronization.
23+
- `code-health`: control flow, naming, map access, and cleanup.
24+
- `api-design`: explicit APIs and dependency boundaries.
25+
- `code-review`: focused changes and actionable reviews.
26+
27+
## Use the linter
28+
29+
Go 1.25 or newer is required.
30+
31+
```bash
32+
go install github.com/gosuda/JustGoodPractices/cmd/gojgp@latest
33+
gojgp lint ./...
34+
```
35+
36+
Run tests and lint together:
37+
38+
```bash
39+
gojgp check
40+
```
41+
42+
Without installing the binary:
43+
44+
```bash
45+
go run github.com/gosuda/JustGoodPractices/cmd/gojgp@latest lint ./...
46+
```
47+
48+
## Develop
49+
50+
```bash
51+
go test ./...
52+
go run ./cmd/gojgp lint ./...
53+
```
54+
55+
All skill metadata follows the Agent Skills specification. Skill instructions are agent-neutral and use repository-relative paths only where appropriate.
56+
57+
## License
58+
59+
[MIT](LICENSE)

cmd/gojgp/main.go

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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, &reg); err != nil {
58+
return nil, fmt.Errorf("failed to parse registry: %w", err)
59+
}
60+
return &reg, 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

Comments
 (0)