ревью: скрипты гейта на python3, шаги выбираются по изменённым файлам
gate.sh и review-context.sh переписаны на python3 — в scripts/ уже жил diff-coverage.py, а разбор вывода git и сборка сводки на shell читались хуже, чем работали. Гейт больше не гоняет go-шаги впустую: build, vet, lint, gofmt, тесты, -race, покрытие и govulncheck запускаются, только если в диффе есть .go либо go.mod/go.sum; миграции — если тронуты миграции или код. Правка документации проходит гейт за секунды вместо минуты. Пропуск при этом не молчит: он в сводке с причиной и уезжает в границы покрытия, а charter гейта различает «код не трогали» (корректно) и «инструмента нет» (настоящая дыра). Изменённые файлы считаем как объединение диффа с базой, рабочего дерева и новых файлов: гейт гоняют и до коммита, и после, а лишний прогон шага дешевле пропущенного. Заодно govulncheck перестал рапортовать «уязвимостей: 0» когда он просто не смог отработать из-за несобирающегося кода — это SKIP, а не WARN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Вход для архитектурного прохода ревью: то, чего нет в диффе.
|
||||
|
||||
Агент, видящий только `git diff`, физически не может судить об архитектуре — он
|
||||
не знает, какие понятия в проекте уже есть и как они называются. Скрипт собирает
|
||||
дерево пакетов с назначением, граф внутренних зависимостей и инвентарь
|
||||
существующих концепций.
|
||||
|
||||
Публичную поверхность пакетов намеренно НЕ выгружаем: дамп `go doc -short` по
|
||||
всему модулю занимал больше половины вывода, а агент вытянет `go doc` по нужному
|
||||
пакету сам. Здесь — только то, что иначе не восстановить.
|
||||
|
||||
Использование: scripts/review-context.py [> tmp/review-context.md]
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def go(*args: str) -> str:
|
||||
return subprocess.run(
|
||||
["go", *args], capture_output=True, text=True, check=True
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def module_path() -> str:
|
||||
for line in Path("go.mod").read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("module "):
|
||||
return line.split(None, 1)[1].strip()
|
||||
return ""
|
||||
|
||||
|
||||
def scan(root: str, pattern: str) -> list[str]:
|
||||
"""Строки нетестовых .go файлов под root, совпавшие с pattern."""
|
||||
re_ = re.compile(pattern)
|
||||
found = set()
|
||||
for path in sorted(Path(root).rglob("*.go")):
|
||||
if path.name.endswith("_test.go"):
|
||||
continue
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if re_.search(line):
|
||||
found.add(line.strip())
|
||||
return sorted(found)
|
||||
|
||||
|
||||
def block(title: str, lines: list[str], lang: str = "") -> None:
|
||||
print(f"### {title}\n")
|
||||
print(f"```{lang}")
|
||||
print("\n".join(lines) if lines else "— пусто")
|
||||
print("```\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
mod = module_path()
|
||||
packages = [p for p in go("list", "./...").splitlines() if not p.endswith("/migrations")]
|
||||
|
||||
print("# Контекст проекта для архитектурного ревью\n")
|
||||
print(f"Сгенерировано `scripts/review-context.py`. Модуль: `{mod}`.\n")
|
||||
|
||||
print("## Пакеты и назначение\n")
|
||||
print("```")
|
||||
for entry in go("list", "-f", "{{.ImportPath}}|{{.Doc}}", "./...").splitlines():
|
||||
path, _, doc = entry.partition("|")
|
||||
short = path.removeprefix(mod + "/")
|
||||
print(f"{short:<34} {doc or '— (нет doc-комментария пакета)'}")
|
||||
print("```\n")
|
||||
|
||||
print("## Граф внутренних зависимостей\n")
|
||||
print("Только импорты внутри модуля. Стрелка A -> B означает «A зависит от B».\n")
|
||||
print("```")
|
||||
for pkg in packages:
|
||||
imports = go("list", "-f", '{{range .Imports}}{{.}}\n{{end}}', pkg).splitlines()
|
||||
deps = sorted({i.removeprefix(mod + "/") for i in imports if i.startswith(mod + "/")})
|
||||
if deps:
|
||||
print(f"{pkg.removeprefix(mod + '/')} -> {' '.join(deps)}")
|
||||
print("```\n")
|
||||
|
||||
print("## Инвентарь концепций\n")
|
||||
print("Как в проекте уже называются вещи. Новое понятие вводим, только"
|
||||
" убедившись,\nчто его нельзя выразить существующими.\n")
|
||||
|
||||
block("Доменные ошибки (sentinel)", scan("internal", r"^var Err\w+ = errors\.New"))
|
||||
block("Состояния загрузки", scan("internal/store", r"State\w+\s+State\s*="))
|
||||
block("Секции конфигурации", scan("internal/config", r'toml:"'))
|
||||
block("Публичные команды воркера (вызываются транспортами)",
|
||||
scan("internal/worker", r"^func \(w \*Worker\) [A-Z]"))
|
||||
block("Capabilities OpenSpec", sorted(p.name for p in Path("openspec/specs").iterdir()))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user