Files
healthlog/scripts/review-context.py
T
av 505664acf1 добавлено окружение разработки: контейнер и гейт ревью
- сервис поднимается через `task up`/`task restart`, данные вынесены в ./data
  и переживают пересборку — поток с телефона не прерывается
- `task gate` гоняет build/vet/lint/test/race/покрытие диффа/миграции, плюс два
  шага под healthlog: данные о здоровье в индексе и синхронность образцов конфига
- `task review:context` собирает вход для архитектурного прохода ревью
2026-08-01 14:11:41 +03:00

119 lines
5.5 KiB
Python
Executable File
Raw 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."""
base = Path(root)
if not base.exists():
return []
re_ = re.compile(pattern)
found = set()
for path in sorted(base.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 listdir(root: str) -> list[str]:
base = Path(root)
return sorted(p.name for p in base.iterdir()) if base.is_dir() else []
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/config", r'toml:"'))
block("Миграции (порядок = порядок эволюции схемы)",
listdir("internal/store/migrations"))
block("Маршруты HTTP", scan("internal/httpapi", r'r\.(Get|Post|Put|Delete|Route|Mount)\('))
block("Слои гранулярности и прочие перечисления домена",
scan("internal", r'^\s*Layer\w+\s+\w*Layer\s*=|^const .*Layer'))
block("Capabilities OpenSpec", listdir("openspec/specs"))
print("## Что держать в голове\n")
print("""healthlog — хранилище, а не аналитика. Инварианты, которые архитектурный
проход обязан защищать (подробно — `CLAUDE.md` и `docs/architecture.md`):
- точки хранятся дословно; всё, что теряет содержимое точки, — находка;
- идентичность по координатам (`метрика + слой + метка`), `source` в ключ
не входит;
- агрегации при записи нет; свёртка живёт только в ответе и только с
измеренным родом метрики;
- нижний слой HAE не суммируется никогда — это интерполяция, а не сэмплы;
- секреты и тела запросов не попадают в логи: данные о здоровье
чувствительнее токенов.
""")
return 0
if __name__ == "__main__":
sys.exit(main())