Add pinned golangci-lint config with number-grounded exclusions and a Makefile battery target as the single entry point, documented in the backend README

This commit is contained in:
heaven 2026-08-04 01:48:58 +03:00
parent 6356471edd
commit 5a81bffb12
3 changed files with 187 additions and 2 deletions

114
backend/.golangci.yml Normal file
View file

@ -0,0 +1,114 @@
# golangci-lint configuration for the backend module.
#
# Every enable/disable is justified by a MEASURED count from the report-only sweep of 2026-08-03
# (golangci-lint 2.12.2, 205 .go files, 115 of them tests). Raw sweep = 435 findings:
# errcheck 413, staticcheck 17, misspell 4, unused 1; govet / ineffassign / nolintlint 0.
# Those counts are the PROVENANCE of each decision below, not the state of the tree: everything they
# name has since been fixed or excluded, so a clean run today reports 0.
#
# Owner's constraint (D39.92) outranks coverage: standards must not slow development or push a
# model into hacks. A rule that produces noise is switched OFF here with its reason on one line —
# never worked around in the code — and nolintlint refuses a bare //nolint.
version: "2"
linters:
default: none
enable:
# --- the standard set ---
- errcheck # 413 raw; the exclusions below account for 401 of them, the rest were fixed
- govet # 0 — already in the manual battery; enabling pins it
- ineffassign # 0
- staticcheck # 17 raw; 6 excluded as QF1001, 11 fixed
- unused # 1 — a real dead method (pipeline.applyBanknote), deleted by this pack
# --- zero-cost bug classes: 0 findings today, so they cost nothing and cannot regress ---
- bodyclose # a leaked provider response body is a transport leak
- copyloopvar
- durationcheck
- makezero
# --- small real tails ---
- errorlint # 3 — wrapped-error comparison is a live correctness class here
- misspell # 4, all one word (see exclusions)
- nilerr # 3
- rowserrcheck # 2
- sqlclosecheck # 1
- nolintlint # 0 today — enforces the anti-hack rule: no silent suppressions
# DISABLED ON PURPOSE (measured, not skipped):
# exhaustruct — 988 findings module-wide. Scoped to the two types it was proposed for
# (config.Stage, pipeline.Request) it is 17, but 16 are test literals and the 17th is the SEAM
# ITSELF (config/internal_call.go Stage()) — the one construction site that is allowed. The
# invariant "Stage is built only through the seam" needs a rule that tells seam from bypass;
# exhaustruct cannot, the analyzer can. Off so it does not compete with that guard.
settings:
staticcheck:
checks:
- all
- -ST1000 # default-off in golangci-lint: package comment form
- -ST1003 # default-off: naming conventions
- -ST1016 # default-off: receiver name consistency
- -ST1020 # default-off: comment form on exported methods
- -ST1021 # default-off: comment form on exported types
- -ST1022 # default-off: comment form on exported vars
- -QF1001 # 6 findings: "apply De Morgan's law". The current shape mirrors the prose above
# each condition ("neither X nor Y"); rewriting for the linter reads worse
# than the rule it encodes. A style opinion, not a defect class.
misspell:
ignore-rules:
# `initals` is a DELIBERATE typo: the langpack tests write it into a fixture to prove the
# parser reports unknown keys instead of silently dropping them. Correcting it deletes the
# test. 1 comment + 3 test sites. NOTE: `locale` is left unset on purpose — setting it to US
# makes misspell rewrite en-GB spellings in the English prose (behaviour, honoured, dialogue):
# measured 0 findings unset against 311 with `locale: US`, none of them real errors.
- initals
nolintlint:
require-explanation: true # a suppression without a reason is the hack this rule exists to stop
require-specific: true # //nolint must name the linter, never blanket-silence a file
allow-unused: false
exclusions:
# Written out instead of the std-error-handling preset: that preset also swallows os.Setenv and
# os.Remove, and one of the two real production findings in this repo is an unchecked os.Setenv.
rules:
# Close/Flush: 294 sites. All 33 production ones are readers or DB handles (listed and
# checked one by one). The repo has exactly ONE flush-on-Close writer — chunktest/epub.go's
# zip.Writer — and its Close IS checked, so this cannot hide a lost write.
- linters: [errcheck]
text: 'Error return value of `[^`]*\.(Close|Flush)` is not checked'
# CLI rendering to stdout: 91 sites. A broken pipe on `tmctl status | head` is not something
# the CLI can act on, and threading it out would rewrite every renderer for nothing.
- linters: [errcheck]
text: 'Error return value of `fmt\.Fprint(f|ln)?` is not checked'
# hash.Hash.Write is documented never to return an error (2 sites, the membank digest). Keyed on
# the METHOD, not on a receiver spelled `h`: a rule that fires for `hasher.Write` but not `h.Write`
# pushes the next contributor toward renaming a variable to appease the linter.
- linters: [errcheck]
text: 'Error return value of `[^`]*\.Write` is not checked'
path: internal/membank/
# `defer tx.Rollback()` — 5 sites. The idiom is "roll back unless committed", so on the happy
# path the error is sql.ErrTxDone BY DESIGN. Wrapping each in a discard closure would add five
# blocks of noise to say nothing. Excluded in config rather than worked around in the code.
- linters: [errcheck]
text: 'Error return value of `[^`]*\.Rollback` is not checked'
# In tests, a response-writer/fixture write that fails cannot be acted on, and a t.Fatal on
# each would bury the assertion the test is actually about.
- linters: [errcheck]
path: _test\.go
text: 'Error return value of `(w\.Write|os\.WriteFile|fmt\.Sscanf)` is not checked'
# runToSignatureStop returns *WaveSignatureStop, which implements error because the stop travels
# the pipeline AS an error — so errcheck sees 12 discarded "errors". The helper has already
# t.Fatal'd on every wrong outcome; the return is there for the tests that want to inspect it.
- linters: [errcheck]
path: internal/pipeline/miningstop_join_test\.go
text: 'Error return value is not checked'
formatters:
enable:
- gofmt # `make fmt` checks this too; the duplicate is deliberate, so a bare
# `golangci-lint run` is still a complete gate on its own
issues:
max-issues-per-linter: 0 # never truncate: a hidden tail reads as "clean"
max-same-issues: 0

60
backend/Makefile Normal file
View file

@ -0,0 +1,60 @@
# Backend battery as one command. `make battery` IS the manual battery every session used to retype;
# it is the entry point CI will call, so what runs locally and what runs there cannot drift.
#
# Run from backend/. Toolchain and linter are PINNED here (never "latest"): the linter's findings are a
# gate, and a gate that changes under you on someone else's machine is not a gate.
GO ?= go
GO_MIN_VERSION := 1.26.4
GOLANGCI_LINT ?= golangci-lint
GOLANGCI_VERSION := 2.12.2
# backend/bin/ is already gitignored, so the vet tool leaves no untracked file behind.
TMVET := bin/tmvet
.PHONY: build vet fmt lint test battery battery-stand tools-check
build: tools-check
$(GO) build ./...
# `-vettool` REPLACES the standard vet suite rather than adding to it (measured: a copylocks finding
# that plain `go vet` reports disappears under -vettool), so both passes have to run. Both tag sets too:
# the paid `live` tests compile only under -tags live and would otherwise rot unseen.
vet:
$(GO) vet ./...
$(GO) vet -tags live ./...
$(GO) build -o $(TMVET) ./cmd/tmvet
$(GO) vet -vettool=$(TMVET) ./...
$(GO) vet -tags live -vettool=$(TMVET) ./...
# `gofmt -l` exits 0 even when it names files, so the emptiness of its output is the assertion.
fmt:
@test -z "$$(gofmt -l .)" || { echo "gofmt: not formatted:"; gofmt -l .; exit 1; }
tools-check:
@$(GO) version | grep -qE 'go1\.(2[6-9]|[3-9][0-9])' || { \
echo "Go $(GO_MIN_VERSION)+ required (go.mod floor); got: $$($(GO) version)"; exit 1; }
@$(GOLANGCI_LINT) --version 2>/dev/null | grep -q " $(GOLANGCI_VERSION) " || { \
echo "golangci-lint $(GOLANGCI_VERSION) required (findings are version-dependent)."; \
echo "install: https://github.com/golangci/golangci-lint/releases/tag/v$(GOLANGCI_VERSION)"; exit 1; }
lint: tools-check
$(GOLANGCI_LINT) run --timeout=15m ./...
# -race needs cgo (a C toolchain). If it is missing this fails loudly — dropping -race would turn a
# missing toolchain into a green run that proved less than it claims.
test:
$(GO) test ./... -race -count=1
# The hermetic battery: everything that is green on a bare clone. It ends by NAMING the tests that did
# not run, because a skip is invisible in `ok` lines and a silent skip reads as coverage.
battery: build vet fmt lint test
@echo "--- did NOT run (no stand data; see battery-stand) ---"
@$(GO) test ./... -count=1 -v > .skips.log 2>&1 || { echo "the skip-harvest pass FAILED:"; \
grep -E '^(---|\s+---) FAIL|^FAIL' .skips.log; rm -f .skips.log; exit 1; }
@grep -- '--- SKIP' .skips.log || echo "(none)"
@rm -f .skips.log
# The stand battery: adds the corpus-gated tests. With the flags set, MISSING data fails instead of
# skipping, so "the corpus is here" is asserted rather than assumed.
battery-stand: build vet fmt lint
TM_MINER_PARITY=1 TM_CHECKER_LABELS=1 $(GO) test ./... -race -count=1

View file

@ -46,9 +46,20 @@
Стенд: WSL2 (localhost из-под прокси = 403 — для local-вызовов no-proxy транспорт). Книга-стенд: `/home/ubuntu/books/gu-zhenren/` (GB18030; тексты и производные ВНЕ git; парити-тесты майнера читают отсюда под `TM_MINER_PARITY=1`).
Батарея — ОДНА команда, `make` из `backend/` (Makefile; собирать её руками больше не нужно):
```bash
make battery # build · vet (оба тег-набора, + архитектурные анализаторы) · gofmt · lint · test -race
# в конце НАЗЫВАЕТ пропущенные тесты — вклеивать в отчёт, не опускать
make battery-stand # то же + корпусные тесты (TM_MINER_PARITY=1 TM_CHECKER_LABELS=1): отсутствие данных ПАДАЕТ
make lint # golangci-lint 2.12.2 (версия пиновата в Makefile; конфиг — .golangci.yml)
```
Линтер ставится отдельно (бинарь вне репо, `make` его НЕ доустанавливает — ссылку печатает `tools-check`).
Архитектурные инварианты живут в `internal/archguard` и запускаются двумя путями: `go vet -vettool` из
`make vet` и обычный `go test ./internal/archguard/` (он прогоняет их по всему дереву).
```bash
go build ./... && go vet ./... && go test ./... -race # зелёное = норма ДЛЯ ГЕРМЕТИЧНОЙ ЧАСТИ (см. ниже)
go test ./... -race -v | grep -- '--- SKIP' # ЧТО НЕ ВЫПОЛНЯЛОСЬ — вклеивать в отчёт, не опускать
go run ./cmd/tmctl translate --config example/book.yaml # реальные вызовы — ключи в .env (пример: zh→ru)
go run ./cmd/tmctl status --config example/book.yaml --json # $0, N/M+паспорта+деньги, живой прогон ок
go run ./cmd/tmctl report --config example/book.yaml # $0, quality-report (KPI/rates)