From 5a81bffb12a6cc3087cc4804407f27aa1530fa08 Mon Sep 17 00:00:00 2001 From: heaven Date: Tue, 4 Aug 2026 01:48:58 +0300 Subject: [PATCH] Add pinned golangci-lint config with number-grounded exclusions and a Makefile battery target as the single entry point, documented in the backend README --- backend/.golangci.yml | 114 ++++++++++++++++++++++++++++++++++++++++++ backend/Makefile | 60 ++++++++++++++++++++++ backend/README.md | 15 +++++- 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 backend/.golangci.yml create mode 100644 backend/Makefile diff --git a/backend/.golangci.yml b/backend/.golangci.yml new file mode 100644 index 00000000..634d795e --- /dev/null +++ b/backend/.golangci.yml @@ -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 diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 00000000..7f80bf82 --- /dev/null +++ b/backend/Makefile @@ -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 diff --git a/backend/README.md b/backend/README.md index cdc4ac97..67481cff 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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)