Non-negotiable structural rules that apply to every Claude Code session in
this repo and to every commit, enforced via three defense-in-depth layers:
1. PreToolUse hook in .claude/settings.json blocks any Write/Edit that
would push a file past the 500-line hard cap. Auto-loads for any
Claude session in this repo regardless of who launched it.
2. scripts/githooks/pre-commit (installed via scripts/install-hooks.sh)
enforces the LOC cap, freezes migrations/ unless [migration-approved],
and protects guardrail files unless [guardrail-change] is present.
3. .gitea/workflows/ci.yaml gets loc-budget + guardrail-integrity jobs,
plus mypy --strict on new Python packages, tsc --noEmit on Node
services, and a syft+grype SBOM scan.
Per-language conventions are documented in AGENTS.python.md / AGENTS.go.md /
AGENTS.typescript.md at the repo root — layering (router->service->repo for
Python, hexagonal for Go, colocation for Next.js), tooling baseline, and
explicit "what you may NOT do" lists.
Adds scripts/check-loc.sh (soft 300 / hard 500, reports 205 hard and 161
soft violations in the current codebase) plus .claude/rules/loc-exceptions.txt
(initially empty — the list is designed to shrink over time).
Per-service READMEs for all 10 services + PHASE1_RUNBOOK.md for the
backend-compliance refactor. Skeleton packages (compliance/{domain,
repositories,schemas}) are the landing zone for the clean-arch rewrite that
begins in Phase 1.
CLAUDE.md is prepended with the six non-negotiable rules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4.5 KiB
4.5 KiB
AGENTS.go.md — Go Service Conventions
Applies to: ai-compliance-sdk/.
Layered architecture (Gin)
Follows Standard Go 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.
│ │ └── <aggregate>/
│ ├── service/ # Business logic. Depends on domain interfaces only.
│ │ └── <aggregate>/
│ ├── repository/postgres/ # Concrete repo implementations.
│ │ └── <aggregate>/
│ ├── transport/http/ # Gin handlers. Thin. One handler per file group.
│ │ ├── handler/<aggregate>/
│ │ ├── 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.
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.Contextas first arg always. Propagate to repos. - Return
(value, error). Wrap withfmt.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/<aggregate>/repository.go. Implementation inrepository/postgres/<aggregate>/. - One file per query group; no file >500 LOC.
- Use
pgx/sqlcover 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:
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.
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
golangci-lintwith:errcheck, govet, staticcheck, revive, gosec, gocyclo (max 15), gocognit (max 20), unused, ineffassign, errorlint, nilerr, nolintlint, contextcheck.gofumptformatting.go vet ./...clean.go mod tidyclean — no unused deps.
Concurrency
- Goroutines must have a clear lifecycle owner (struct method that started them must stop them).
- Pass
ctxeverywhere. Cancellation respected. - No global mutexes for request data. Use per-request context.
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 ininternal/app. - Create a file >500 lines.
- Change a public route's contract without updating consumers.