# 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 |