# AGENTS.go.md — Go Service Conventions Applies to: `ai-compliance-sdk/`. ## Layered architecture (Gin) Follows [Standard Go Project Layout](https://github.com/golang-standards/project-layout) + hexagonal/clean-arch. ``` ai-compliance-sdk/ ├── cmd/server/main.go # Thin: parse flags → app.New → app.Run. <50 LOC. ├── internal/ │ ├── app/ # Wiring: config + DI graph + lifecycle. │ ├── domain/ # Pure types, interfaces, errors. No I/O imports. │ │ └── / │ ├── service/ # Business logic. Depends on domain interfaces only. │ │ └── / │ ├── repository/postgres/ # Concrete repo implementations. │ │ └── / │ ├── transport/http/ # Gin handlers. Thin. One handler per file group. │ │ ├── handler// │ │ ├── middleware/ │ │ └── router.go │ └── platform/ # DB pool, logger, config, tracing. └── pkg/ # Importable by other repos. Empty unless needed. ``` **Dependency direction:** `transport → service → domain ← repository`. `domain` imports nothing from siblings. ## Handlers - One handler = one Gin function. ≤40 LOC. - Bind → call service → map domain error to HTTP via `httperr.Write(c, err)` → respond. - Return early on errors. No business logic, no SQL. ```go func (h *IACEHandler) Create(c *gin.Context) { var req CreateIACERequest if err := c.ShouldBindJSON(&req); err != nil { httperr.Write(c, httperr.BadRequest(err)) return } out, err := h.svc.Create(c.Request.Context(), req.ToInput()) if err != nil { httperr.Write(c, err) return } c.JSON(http.StatusCreated, out) } ``` ## Services - Struct + constructor + interface methods. No package-level state. - Take `context.Context` as first arg always. Propagate to repos. - Return `(value, error)`. Wrap with `fmt.Errorf("create iace: %w", err)`. - Domain errors implemented as sentinel vars or typed errors; matched with `errors.Is` / `errors.As`. ## Repositories - Interface lives in `domain//repository.go`. Implementation in `repository/postgres//`. - One file per query group; no file >500 LOC. - Use `pgx`/`sqlc` over hand-rolled string SQL when feasible. No ORM globals. - All queries take `ctx`. No background goroutines without explicit lifecycle. ## Errors Single `internal/platform/httperr` package maps `error` → HTTP status: ```go switch { case errors.Is(err, domain.ErrNotFound): return 404 case errors.Is(err, domain.ErrConflict): return 409 case errors.As(err, &validationErr): return 422 default: return 500 } ``` Never `panic` in request handling. `recover` middleware logs and returns 500. ## Tests - Co-located `*_test.go`. - **Table-driven** tests for service logic; use `t.Run(tt.name, ...)`. - Handlers tested with `httptest.NewRecorder`. - Repos tested with `testcontainers-go` (or the existing compose Postgres) — never mocks at the SQL boundary. - Coverage target: 80% on `service/`. CI fails on regression. ```go func TestIACEService_Create(t *testing.T) { tests := []struct { name string input service.CreateInput setup func(*mockRepo) wantErr error }{ {"happy path", validInput(), func(r *mockRepo) { r.createReturns(nil) }, nil}, {"conflict", validInput(), func(r *mockRepo) { r.createReturns(domain.ErrConflict) }, domain.ErrConflict}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { /* ... */ }) } } ``` ## Tooling Run lint before pushing: ```bash golangci-lint run --timeout 5m ./... ``` The `.golangci.yml` at the service root (`ai-compliance-sdk/.golangci.yml`) enables: `errcheck, govet, staticcheck, gosec, gocyclo (≤20), gocritic, revive, goimports, unused, ineffassign`. Fix lint violations in new code; legacy violations are tracked but not required to fix immediately. - `gofumpt` formatting. - `go vet ./...` clean. - `go mod tidy` clean — no unused deps. ## File splitting pattern When a Go file exceeds the 500-line hard cap, split it in place — no new packages needed: - All split files stay in **the same package directory** with the **same `package ` declaration**. - No import changes are needed anywhere because Go packages are directory-scoped. - Naming: `store_projects.go`, `store_components.go` (noun + underscore + sub-resource). - For handlers: `iace_handler_projects.go`, `iace_handler_hazards.go`, etc. - Before splitting, add a characterization test that pins current behaviour. ## Error handling Domain errors are defined in `internal/domain//errors.go` as sentinel vars or typed errors. The mapping from domain error to HTTP status lives exclusively in `internal/platform/httperr/httperr.go` via `errors.Is` / `errors.As`. Handlers call `httperr.Write(c, err)` — **never** directly call `c.JSON` with a status code derived from business logic. ## Context propagation - Always pass `ctx context.Context` as the **first parameter** in every service and repository method. - Never store a context in a struct field — pass it per call. - Cancellation must be respected: check `ctx.Err()` in loops; propagate to all I/O calls. ## Concurrency - Goroutines must have a clear lifecycle owner (struct method that started them must stop them). - Pass `ctx` everywhere. Cancellation respected. - No global mutexes for request data. Use per-request context. ## Before every push — MANDATORY Run all steps for `ai-compliance-sdk/` before pushing. CI runs the same checks and will fail if you skip this. ```bash cd ai-compliance-sdk # 1. Vet + lint go vet ./... golangci-lint run --timeout 5m ./... # 2. Tests go test ./... # 3. Build go build ./... ``` All steps must exit 0. Do not push if any step fails. ## What you may NOT do - Touch DB schema/migrations. - Add a new top-level package directly under `internal/` without architectural review. - `import "C"`, unsafe, reflection-heavy code. - Use `init()` for non-trivial setup. Wire it in `internal/app`. - Use `interface{}` / `any` in new code without an explicit comment justifying it. - Call `log.Fatal` outside of `main.go`; panicking in request handling is also forbidden. - Shadow `err` with `:=` inside an `if`-block when the outer scope already declares `err` — use `=` or rename. - Create a file >500 lines. - Change a public route's contract without updating consumers.