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:
parent
6356471edd
commit
5a81bffb12
3 changed files with 187 additions and 2 deletions
114
backend/.golangci.yml
Normal file
114
backend/.golangci.yml
Normal 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
60
backend/Makefile
Normal 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
|
||||||
|
|
@ -46,9 +46,20 @@
|
||||||
|
|
||||||
Стенд: WSL2 (localhost из-под прокси = 403 — для local-вызовов no-proxy транспорт). Книга-стенд: `/home/ubuntu/books/gu-zhenren/` (GB18030; тексты и производные ВНЕ git; парити-тесты майнера читают отсюда под `TM_MINER_PARITY=1`).
|
Стенд: 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
|
```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 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 status --config example/book.yaml --json # $0, N/M+паспорта+деньги, живой прогон ок
|
||||||
go run ./cmd/tmctl report --config example/book.yaml # $0, quality-report (KPI/rates)
|
go run ./cmd/tmctl report --config example/book.yaml # $0, quality-report (KPI/rates)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue