Files
jellybit/scripts/review-context.py
avandClaude Opus 4.8 776a1ca6b6 ревью: скрипты гейта на 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>
2026-07-23 19:50:40 +03:00

95 lines
4.2 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())