- docs/MAINTAINABILITY.md: limits and architecture rules - .golangci.yml (pinned v2.14.0) + file-size check; make lint runs first in every gate - web/go.mod stops the Go tool from walking web/node_modules - layering: Target -> internal/snmptarget, TableKey -> profile.Metric.TableKey; poller no longer imports discovery Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
3.4 KiB
3.4 KiB
Maintainability standard
This is a hard rule (owner decision, 2026-10-22). Code that breaks it does not merge, and a milestone with findings is not tagged. The machine enforces it: make lint runs golangci-lint with .golangci.yml (Go) and ESLint (web). Every scripts/gate-m*.sh runs make lint first, and so does CI.
Limits (production code)
| What | Limit | Enforced by |
|---|---|---|
| Function length | 80 lines, 50 statements | funlen |
| Cyclomatic complexity | 15 per function | cyclop |
| Cognitive complexity | 20 per function | gocognit |
| Nesting | if depth 4 |
nestif |
| Duplicated blocks | 150 tokens | dupl |
| Line length | 140 characters | lll |
| File length | 600 lines (split by concern before that) | scripts/lint-filesize.sh |
| Unused code, dead fields | none | unused |
| Deprecated APIs | none | staticcheck |
| Unchecked errors | none | errcheck, errorlint |
| Magic HTTP status numbers | none (use http.Status*) |
usestdlibvars |
Tests (_test.go) are exempt from the length and complexity limits, because table-driven tests are long by nature. They must still pass every other linter.
Architecture rules (enforced by depguard and forbidigo)
- SQL and the ORM live only in
internal/store. No other package importsgithub.com/uptrace/bunordatabase/sql. - Layering:
cmd → app → {api, poller, discovery, traps, alert, notify, …} → {store, snmp, profile, metrics, clock, …}.- Only
cmd/*importsinternal/app. - Only
internal/app(and tests) importsinternal/api. - Collection paths do not import each other:
poller,discovery,traps,ingest,checks,rollupandalertshare code through a lower package such asinternal/snmpx, not through each other. cmd/nmonnever importsinternal/librenms(the importer is a separate tool).
- Only
- Time comes from
internal/clock.time.Now,time.Sleep,time.After,time.Tickandtime.NewTickerare forbidden outsideinternal/clock,cmd/and tests. - No global mutable state except the
selfmon.Defaultregistry and package-level caches that are documented as such.
Habits the linters can't check (reviewers enforce these)
- One concern per file. Name files after what they hold (
matcher.go, notutil.go). Noutil,helpersorcommonpackages. - Split a long function into named steps that read like the spec (
collect → match → apply → audit), not intodoPart1/doPart2. - Errors are wrapped with context (
fmt.Errorf("discovery %s: %w", slug, err)) and handled once. - Every exported identifier has a doc comment that says why or what contract, not a restatement of the name.
- Comments explain decisions, not mechanics. A decision that departs from the spec goes in
docs/DEVIATIONS.md. - Tests are named for the behaviour they lock in (
TestStackSwapOpensOneReviewAndNeverMerges). //nolintis allowed only with the linter named and a reason (//nolint:funlen // generated table).nolintlintenforces this.- New dependencies need a line in
docs/DEVIATIONS.mdrow 3 saying why the standard library isn't enough.
Web (web/)
ESLint with typescript-eslint (strict), max-lines-per-function 80, complexity 15, max-depth 4, max-lines 400 per file, no any, react-hooks rules. TrafficGraph.tsx is the only file that imports uPlot. API types are only generated, never hand-written.