Some checks failed
CI / go-lint (push) Has been cancelled
CI / python-lint (push) Has been cancelled
CI / nodejs-lint (push) Has been cancelled
CI / test-go-consent (push) Has been cancelled
CI / test-python-voice (push) Has been cancelled
CI / test-bqas (push) Has been cancelled
Mandatory pre-push gates for all three language stacks with exact commands, common pitfalls, and architecture rules. CLAUDE.md updated with quick-reference section linking to the new files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
228 lines
6.6 KiB
Markdown
228 lines
6.6 KiB
Markdown
# AGENTS.go.md — Go Agent Rules
|
|
|
|
Applies to: `ai-compliance-sdk/` (Go/Gin service)
|
|
|
|
---
|
|
|
|
## NON-NEGOTIABLE: Pre-Push Checklist
|
|
|
|
**BEFORE every `git push`, run ALL of the following from the module root. A single failure blocks the push.**
|
|
|
|
```bash
|
|
# 1. Format (gofmt is non-negotiable — unformatted code fails CI)
|
|
gofmt -l . | grep -q . && echo "FORMATTING ERRORS — run: gofmt -w ." && exit 1 || true
|
|
|
|
# 2. Vet (catches suspicious code that compiles but is likely wrong)
|
|
go vet ./...
|
|
|
|
# 3. Lint (golangci-lint aggregates 50+ linters — the de-facto standard)
|
|
golangci-lint run --timeout=5m ./...
|
|
|
|
# 4. Tests with race detector
|
|
go test -race -count=1 ./...
|
|
|
|
# 5. Build verification (catches import errors, missing implementations)
|
|
go build ./...
|
|
```
|
|
|
|
**One-liner pre-push gate:**
|
|
```bash
|
|
gofmt -l . | grep -q . && exit 1; go vet ./... && golangci-lint run --timeout=5m && go test -race -count=1 ./... && go build ./...
|
|
```
|
|
|
|
### Why each check matters
|
|
|
|
| Check | Catches | Time |
|
|
|-------|---------|------|
|
|
| `gofmt` | Formatting violations (CI rejects unformatted code) | <1s |
|
|
| `go vet` | Printf format mismatches, unreachable code, shadowed vars | <5s |
|
|
| `golangci-lint` | 50+ static analysis checks (errcheck, staticcheck, etc.) | 10-30s |
|
|
| `go test -race` | Race conditions (invisible without this flag) | 10-60s |
|
|
| `go build` | Import errors, interface mismatches | <5s |
|
|
|
|
---
|
|
|
|
## golangci-lint Configuration
|
|
|
|
Config lives in `.golangci.yml` at the repo root. Minimum required linters:
|
|
|
|
```yaml
|
|
linters:
|
|
enable:
|
|
- errcheck # unchecked errors are bugs
|
|
- gosimple # code simplification
|
|
- govet # go vet findings
|
|
- ineffassign # useless assignments
|
|
- staticcheck # advanced static analysis (SA*, S*, QF*)
|
|
- unused # unused code
|
|
- gofmt # formatting
|
|
- goimports # import organization
|
|
- gocritic # opinionated style checks
|
|
- noctx # HTTP requests without context
|
|
- bodyclose # unclosed HTTP response bodies
|
|
- exhaustive # exhaustive switch on enums
|
|
- wrapcheck # errors from external packages must be wrapped
|
|
|
|
linters-settings:
|
|
errcheck:
|
|
check-blank: true # blank identifier for errors is a bug
|
|
govet:
|
|
enable-all: true
|
|
|
|
issues:
|
|
max-issues-per-linter: 0
|
|
max-same-issues: 0
|
|
```
|
|
|
|
**Never suppress with `//nolint:` without a comment explaining why it's safe.**
|
|
|
|
---
|
|
|
|
## Code Structure (Hexagonal Architecture)
|
|
|
|
```
|
|
ai-compliance-sdk/
|
|
├── cmd/
|
|
│ └── server/main.go # thin: parse flags, wire deps, call app.Run()
|
|
├── internal/
|
|
│ ├── app/ # dependency wiring
|
|
│ ├── domain/ # pure business logic, no framework deps
|
|
│ ├── ports/ # interfaces (repositories, external services)
|
|
│ ├── adapters/
|
|
│ │ ├── http/ # Gin handlers (≤30 LOC per handler)
|
|
│ │ ├── postgres/ # DB adapters implementing ports
|
|
│ │ └── external/ # third-party API clients
|
|
│ └── services/ # orchestration between domain + ports
|
|
└── pkg/ # exported, reusable packages
|
|
```
|
|
|
|
**Handler constraint — max 30 lines per handler:**
|
|
```go
|
|
func (h *RiskHandler) GetRisk(c *gin.Context) {
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
risk, err := h.service.Get(c.Request.Context(), id)
|
|
if err != nil {
|
|
h.handleError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, risk)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
```go
|
|
// REQUIRED: wrap errors with context
|
|
if err != nil {
|
|
return fmt.Errorf("get risk %s: %w", id, err)
|
|
}
|
|
|
|
// REQUIRED: define sentinel errors in domain package
|
|
var ErrNotFound = errors.New("not found")
|
|
var ErrUnauthorized = errors.New("unauthorized")
|
|
|
|
// REQUIRED: check errors — never use _ for error returns
|
|
result, err := service.Do(ctx, input)
|
|
if err != nil {
|
|
// handle it
|
|
}
|
|
```
|
|
|
|
**`errcheck` linter enforces this — zero tolerance for unchecked errors.**
|
|
|
|
---
|
|
|
|
## Testing Requirements
|
|
|
|
```
|
|
internal/
|
|
├── domain/
|
|
│ ├── risk.go
|
|
│ └── risk_test.go # unit: pure functions, no I/O
|
|
├── adapters/
|
|
│ ├── http/
|
|
│ │ ├── handler.go
|
|
│ │ └── handler_test.go # httptest-based, mock service
|
|
│ └── postgres/
|
|
│ ├── repo.go
|
|
│ └── repo_test.go # integration: testcontainers or real DB
|
|
```
|
|
|
|
**Test naming convention:**
|
|
```go
|
|
func TestRiskService_Get_ReturnsRisk(t *testing.T) {}
|
|
func TestRiskService_Get_NotFound_ReturnsError(t *testing.T) {}
|
|
func TestRiskService_Get_DBError_WrapsError(t *testing.T) {}
|
|
```
|
|
|
|
**Table-driven tests are mandatory for functions with multiple cases:**
|
|
```go
|
|
func TestValidateInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantErr bool
|
|
}{
|
|
{"valid", "ok", false},
|
|
{"empty", "", true},
|
|
{"too long", strings.Repeat("x", 300), true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateInput(tt.input)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
```bash
|
|
# Pre-push: unit tests only (fast)
|
|
go test -race -count=1 -run "^TestUnit" ./...
|
|
|
|
# CI: all tests
|
|
go test -race -count=1 -coverprofile=coverage.out ./...
|
|
go tool cover -func=coverage.out | grep total
|
|
```
|
|
|
|
---
|
|
|
|
## Context Propagation
|
|
|
|
Every function that does I/O (DB, HTTP, file) **must** accept and pass `context.Context` as the first argument:
|
|
|
|
```go
|
|
// REQUIRED
|
|
func (r *RiskRepo) Get(ctx context.Context, id uuid.UUID) (*Risk, error) {
|
|
return r.db.QueryRowContext(ctx, query, id).Scan(...)
|
|
}
|
|
|
|
// FORBIDDEN — no context
|
|
func (r *RiskRepo) Get(id uuid.UUID) (*Risk, error) { ... }
|
|
```
|
|
|
|
`noctx` linter enforces HTTP client context. Manual review required for DB calls.
|
|
|
|
---
|
|
|
|
## Common Pitfalls That Break CI
|
|
|
|
| Pitfall | Prevention |
|
|
|---------|------------|
|
|
| Unformatted code | `gofmt -w .` before commit |
|
|
| Unchecked error return from `rows.Close()` / `resp.Body.Close()` | `errcheck` + `bodyclose` linters |
|
|
| Goroutine leak (goroutine started but never stopped) | `-race` test flag |
|
|
| Shadowed `err` variable in nested scope | `govet -shadow` |
|
|
| HTTP response body not closed | `bodyclose` linter |
|
|
| `interface{}` instead of `any` (Go 1.18+) | `gocritic` |
|
|
| Missing context on DB/HTTP calls | `noctx` linter |
|
|
| Returning concrete type from constructor instead of interface | breaks testability |
|