Normative rules for code in this repository. AGENTS.md states the principles;
this document states the specifics.
Enforced, in order, by make ci:
| Gate | Tool | Command |
|---|---|---|
| Format | gofmt, gofumpt, goimports |
make fmt-check |
| Static analysis | go vet |
make vet |
| Lint | golangci-lint (pinned v2) |
make lint |
A lint suppression must be specific and explained:
//nolint:gosec // G404: this PRNG seeds a load-balancing shuffle, not a secret.
Blanket suppressions, or suppressions without a reason, fail the nolintlint
check.
panic is reserved for programmer errors in
init or genuinely unreachable states.%w: fmt.Errorf("loading config: %w", err).errors.Is / errors.As. Never compare error strings.var ErrFoo = errors.New("...") values and
are wrapped, never replaced._ = only with a comment stating why it is safe.io.Reader, http.Handler) over wide ones.any (not interface{}) for genuinely untyped values and narrow
immediately.context.Context, a done channel, or a WaitGroup).context.Context is the first parameter; it is never stored in a struct.-race test.| Kind | Convention | Example |
|---|---|---|
| Package | short, lower-case, no underscores | envconfig |
| Exported identifier | PascalCase, doc comment required | Server, NewServer |
| Unexported identifier | camelCase | parseFlags |
| Acronym | all caps inside the name | HTTPServer, UserID |
| Test file | <file>_test.go |
main_test.go |
| Test helper package | internal/testutil (test-only, depguard-enforced) |
| Path | Purpose |
|---|---|
internal/ |
private code; not importable from outside the module |
cmd/<binary>/ |
one directory per binary when there is more than one |
pkg/ |
code deliberately offered for reuse (optional, rare) |
testdata/ |
fixtures; ignored by the Go toolchain |
web/ |
frontend workspace, added when the UI milestone lands |
main packages contain wiring only: parse flags, read configuration, construct
dependencies, hand off, handle shutdown. Logic lives in importable packages so it
can be tested without spawning a process.
go.mod; commit go.sum.web/, when it lands)strict: true, plus noUncheckedIndexedAccess and exactOptionalPropertyTypes.any. Use unknown and narrow with type guards.interface.useState / useReducer locally, context for cross-cutting concerns.
No global mutable singletons.style objects.tsc --noEmit must be clean.