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