ревью: детерминированный гейт task gate и карта проекта для архитектурного прохода

scripts/gate.sh гонит всё, у чего есть объективный оракул (build, vet, lint,
gofmt, тесты, повтор на флаки, -race, покрытие изменённых строк, миграции,
ER-схема по диффу, gitleaks, govulncheck), не останавливаясь на первом отказе:
ревью нужна полная картина. Пропущенный шаг попадает в сводку — молча
пропущенная проверка даёт ложное ощущение проверенности.

scripts/diff-coverage.py считает покрытие именно изменённых строк: общий
процент по пакету для ревью бесполезен.

scripts/review-context.sh собирает вход, которого нет в диффе — пакеты с
назначением, граф внутренних зависимостей, публичную поверхность и инвентарь
концепций. Агент, видящий только дифф, не знает словаря проекта и потому не
может судить об архитектуре.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-23 18:17:28 +03:00
co-authored by Claude Opus 4.8
parent 1d6f8f3449
commit 6792f7082a
4 changed files with 355 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env bash
# Детерминированный гейт ревью: прогоняет всё, у чего есть объективный оракул,
# и печатает сводку. В отличие от `task test`/`task lint` не останавливается на
# первом отказе — ревьюверу нужна полная картина, а не первая упавшая команда.
#
# Использование: scripts/gate.sh [<base-rev>]
# base-rev — база для диффа (покрытие изменённых строк). По умолчанию
# определяется как merge-base с master; для самого master — HEAD~1.
#
# Коды возврата: 0 — все обязательные шаги зелёные, 1 — есть красные.
# Пропущенные шаги (нет инструмента) НЕ красят гейт, но попадают в сводку:
# молча пропущенная проверка — это ложное ощущение проверенности.
set -u
OUT_DIR="tmp/gate"
mkdir -p "$OUT_DIR"
BASE="${1:-}"
if [ -z "$BASE" ]; then
if git rev-parse --verify -q master >/dev/null && \
[ "$(git rev-parse --abbrev-ref HEAD)" != "master" ]; then
BASE="$(git merge-base HEAD master)"
else
BASE="HEAD~1"
fi
fi
FAILED=0
SUMMARY=""
record() { # <status> <name> <hint>
SUMMARY="${SUMMARY}$1\t$2\t$3\n"
[ "$1" = "FAIL" ] && FAILED=1
return 0
}
step() { # <name> <hint-on-fail> <cmd...>
local name="$1" hint="$2"
shift 2
if "$@" >"$OUT_DIR/$name.log" 2>&1; then
record OK "$name" ""
else
record FAIL "$name" "$hint$OUT_DIR/$name.log"
fi
}
echo "== gate: база диффа $BASE =="
# --- Компиляция и статика ---
step build "не собирается" go build ./...
step vet "" go vet ./...
if command -v golangci-lint >/dev/null; then
step lint "" golangci-lint run
else
record SKIP lint "golangci-lint не установлен (task setup)"
fi
gofmt_out="$(gofmt -l . 2>/dev/null | grep -v '^tmp/' || true)"
if [ -n "$gofmt_out" ]; then
record FAIL gofmt "не отформатировано: $(echo "$gofmt_out" | tr '\n' ' ')"
else
record OK gofmt ""
fi
# --- Тесты ---
step test "" go test -count=1 ./...
# Флаки: повторный прогон тех же тестов. Расхождение результатов — находка не
# ниже major (тест, который иногда зелёный, не является оракулом ни для чего).
if [ -f "$OUT_DIR/test.log" ] && grep -q '^ok\|^---' "$OUT_DIR/test.log"; then
if go test -count=1 ./... >"$OUT_DIR/test-repeat.log" 2>&1; then
if grep -q "^FAIL" "$OUT_DIR/test.log"; then
record FAIL flaky "прогон 1 красный, прогон 2 зелёный — флаки-тест"
else
record OK flaky ""
fi
else
if grep -q "^FAIL" "$OUT_DIR/test.log"; then
record OK flaky "" # стабильно красный — это не флаки, отработает шаг test
else
record FAIL flaky "прогон 1 зелёный, прогон 2 красный — флаки-тест"
fi
fi
fi
# --- Гонки ---
if command -v gcc >/dev/null; then
step race "детектор гонок сработал" env CGO_ENABLED=1 go test -race -count=1 ./...
else
record SKIP race "нет gcc: -race требует cgo. Гонки НЕ проверены — в границы покрытия"
fi
# --- Покрытие изменённых строк ---
if go test -count=1 -coverprofile="$OUT_DIR/cover.out" ./... >"$OUT_DIR/cover.log" 2>&1; then
if python3 scripts/diff-coverage.py "$OUT_DIR/cover.out" "$BASE" \
>"$OUT_DIR/diff-coverage.log" 2>&1; then
record OK diff-coverage "$(head -1 "$OUT_DIR/diff-coverage.log")"
else
record SKIP diff-coverage "не удалось посчитать → $OUT_DIR/diff-coverage.log"
fi
else
record SKIP diff-coverage "прогон с профилем не собрался → $OUT_DIR/cover.log"
fi
# --- Миграции на чистой схеме ---
step migrations "миграции не накатываются с нуля" \
go test -count=1 -run Migration ./internal/store/...
# --- ER-схема синхронна с миграциями ---
# docs/conventions/database.md: структуру меняем — обновляем ER-схему в том же
# change. Проверка по диффу, поэтому живёт здесь, а не в internal/archrules.
changed="$(git diff --name-only "$BASE"...HEAD 2>/dev/null || true)"
if echo "$changed" | grep -q '^internal/store/migrations/'; then
if echo "$changed" | grep -q '^docs/specs/database.md$'; then
record OK er-schema ""
else
record FAIL er-schema "миграция изменена, а docs/specs/database.md — нет"
fi
fi
# --- Секреты ---
if command -v gitleaks >/dev/null; then
step gitleaks "возможен секрет в истории/индексе" gitleaks git --no-banner
else
record SKIP gitleaks "gitleaks не установлен"
fi
# --- Уязвимости зависимостей ---
if command -v govulncheck >/dev/null; then
step govulncheck "" govulncheck ./...
else
record SKIP govulncheck "govulncheck не установлен (go install golang.org/x/vuln/cmd/govulncheck@latest)"
fi
echo
echo "== сводка =="
printf "%b" "$SUMMARY" | while IFS=$'\t' read -r status name hint; do
printf "%-5s %-14s %s\n" "$status" "$name" "$hint"
done
echo
if [ "$FAILED" -eq 1 ]; then
echo "ГЕЙТ КРАСНЫЙ — опиниативные проходы не запускаются"
exit 1
fi
echo "гейт зелёный (пропущенные шаги см. в сводке — они идут в границы покрытия)"