Files
nmon/docs/MAINTAINABILITY.md
jleung f7b394585b Maintainability is a hard rule: standard, golangci-lint config, lint in every gate
- 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
2026-10-22 18:54:32 +00:00

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 imports github.com/uptrace/bun or database/sql.
  • Layering: cmd → app → {api, poller, discovery, traps, alert, notify, …} → {store, snmp, profile, metrics, clock, …}.
    • Only cmd/* imports internal/app.
    • Only internal/app (and tests) imports internal/api.
    • Collection paths do not import each other: poller, discovery, traps, ingest, checks, rollup and alert share code through a lower package such as internal/snmpx, not through each other.
    • cmd/nmon never imports internal/librenms (the importer is a separate tool).
  • Time comes from internal/clock. time.Now, time.Sleep, time.After, time.Tick and time.NewTicker are forbidden outside internal/clock, cmd/ and tests.
  • No global mutable state except the selfmon.Default registry 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, not util.go). No util, helpers or common packages.
  • Split a long function into named steps that read like the spec (collect → match → apply → audit), not into doPart1/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).
  • //nolint is allowed only with the linter named and a reason (//nolint:funlen // generated table). nolintlint enforces this.
  • New dependencies need a line in docs/DEVIATIONS.md row 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.