Files
avandClaude Opus 5 bd1ea6d6b1 старшинство диаграмм объявлено, рендер проверяется скриптом
Диаграмма и проза вокруг неё описывают один факт — это второй дом, и
разойтись они могут молча: то самое, против чего написан copies.py.
Механической сверки здесь нет, дословного соответствия между текстом и
графом не существует, поэтому работает объявление. В review-pipeline
старший граф — он и есть алгоритм планировщика, проза объясняет рёбра;
в остальных местах старшая проза, диаграмма там сводка; в calibration.md
старшая таблица вердиктов, схема добавляет к ней только счётчик.

Объявление стоит у каждой диаграммы строкой в месте, а не общим правилом
в README: скилл читают целиком, README — нет. В task-batch добавлена
оговорка про соседний скилл — два вызова с разным старшинством рядом это
место, где легко ошибиться.

scripts/diagrams.py вынимает все mermaid-блоки и рендерит каждый через
mmdc или npx @mermaid-js/mermaid-cli. Коды выхода — общий словарь; нет
рендерера — код 3, а не молчаливый успех. Chromium с --no-sandbox:
без флага падает на «No usable sandbox», причина в докстроке. Проверены
обе ветки: 11 диаграмм в 9 файлах зелено, сломанный блок даёт точное
место с текстом ошибки парсера и код 1.

README: раздел «Проверка диаграмм» рядом с проверкой копий — что ловит,
чего не ловит и почему не в гейте. DECISIONS 63 и 64.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 20:41:42 +03:00

189 lines
8.7 KiB
Python
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
"""Проверка mermaid-диаграмм в markdown этого репозитория.
Диаграммы заведены там, где структура — граф или автомат: порядок проходов
ревью, жизненный цикл записи по индексам, исходы задачи в спринте, храповик
промоута, счётчик калибровки, граф вызовов между плагинами.
Проверка нужна по одной причине: **синтаксическая ошибка в блоке не видна при
чтении**. Текст диаграммы выглядит правдоподобно, `git diff` показывает разумную
строку, ревью её пропускает — а отрендерить не удаётся, и читатель видит вместо
схемы полотно исходника либо сообщение об ошибке. Первый раз это ловилось тем,
что автор не забыл прогнать рендер руками; на «не забыл» проверки не строятся.
Чего проверка **не** ловит: расхождение диаграммы с прозой вокруг неё. Это
второй дом для одного факта, и удержать его в синхроне может только правило
старшинства, записанное рядом с каждой диаграммой (в конвейере ревью старший —
граф, он и есть алгоритм; в остальных местах старшая — проза, диаграмма там
сводка). Механической сверки для этого нет: между текстом и графом нет
дословного соответствия, которое можно было бы сличить, — в отличие от копий
правил, где оно есть и где его сверяет copies.py.
Рендерит `mermaid-cli`: бинарь `mmdc`, если он на PATH, иначе
`npx --yes @mermaid-js/mermaid-cli`. Ни того ни другого нет — это код 3, а не
молчаливый успех: проверка, отчитавшаяся «диаграммы в порядке», ничего не
отрендерив, хуже отсутствующей.
Chromium запускается с `--no-sandbox`: на современных дистрибутивах
непривилегированные user namespaces выключены, и без флага рендер падает на
«No usable sandbox». Содержимое здесь своё и локальное, так что песочница ничего
не защищает — она только мешает запуску.
Коды выхода — тот же словарь, что у tasks.py, docs.py и copies.py:
0 все диаграммы рендерятся
1 диаграмма не рендерится
2 ошибка употребления: аргументы
3 окружение: не тот каталог, нет mermaid-cli
4 внутренний сбой
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
FENCE_OPEN = re.compile(r"^\s*```mermaid\s*$")
FENCE_CLOSE = re.compile(r"^\s*```\s*$")
# Каталоги, где markdown не наш: клоны, окружения, временное.
SKIP = {".git", ".venv", "node_modules", "tmp", ".ruff_cache"}
# Стек вызовов puppeteer/mermaid полезного не несёт — режем по первой его строке.
NOISE = re.compile(r"^(Parser\.|\s+at )")
# Строки хода работы: не ошибка, а прогресс рендера.
PROGRESS = re.compile(r"^(Generating|Found \d+ mermaid)")
class Block:
"""Один блок ```mermaid: где начался и что внутри."""
def __init__(self, path: Path, line: int, text: str, root: Path) -> None:
self.path = path
self.line = line
self.text = text
self.where = f"{path.relative_to(root).as_posix()}:{line}"
def collect(root: Path) -> list[Block]:
"""Все mermaid-блоки репозитория, в порядке обхода."""
found: list[Block] = []
for path in sorted(root.rglob("*.md")):
if any(part in SKIP for part in path.relative_to(root).parts):
continue
lines = path.read_text(encoding="utf-8").splitlines()
start: int | None = None
body: list[str] = []
for i, line in enumerate(lines, start=1):
if start is None:
if FENCE_OPEN.match(line):
start, body = i, []
continue
if FENCE_CLOSE.match(line):
found.append(Block(path, start, "\n".join(body) + "\n", root))
start = None
continue
body.append(line)
if start is not None:
found.append(Block(path, start, "\n".join(body) + "\n", root))
return found
def renderer() -> list[str] | None:
"""Команда рендера или None, если mermaid-cli недоступен."""
mmdc = shutil.which("mmdc")
if mmdc:
return [mmdc]
if shutil.which("npx"):
return ["npx", "--yes", "@mermaid-js/mermaid-cli"]
return None
def render(cmd: list[str], block: Block, workdir: Path, config: Path) -> str | None:
"""Отрендерить блок. None — получилось, иначе сообщение об ошибке."""
src = workdir / "d.mmd"
src.write_text(block.text, encoding="utf-8")
out = workdir / "d.svg"
done = subprocess.run(
[*cmd, "-p", str(config), "-i", str(src), "-o", str(out)],
capture_output=True,
text=True,
check=False,
)
if done.returncode == 0 and out.exists():
out.unlink()
return None
noise = f"{done.stdout}\n{done.stderr}"
useful = []
for line in noise.splitlines():
if NOISE.match(line):
break
if line.strip() and not PROGRESS.match(line.strip()):
useful.append(line.strip())
return "\n ".join(useful[:8]) or f"код {done.returncode} без сообщения"
def main() -> int:
ap = argparse.ArgumentParser(description="Проверка mermaid-диаграмм.")
ap.add_argument("--dir", default=".", help="корень репозитория")
args = ap.parse_args()
root = Path(args.dir).resolve()
if not (root / ".claude-plugin").is_dir():
print(f"окружение: {root} не похож на корень репозитория"
f" (нет .claude-plugin)", file=sys.stderr)
return ENV
blocks = collect(root)
if not blocks:
print("диаграмм нет")
return OK
cmd = renderer()
if cmd is None:
print("окружение: нет ни mmdc, ни npx — рендер невозможен."
" Поставь mermaid-cli (npm i -g @mermaid-js/mermaid-cli)"
" или запусти проверку там, где есть npx.", file=sys.stderr)
return ENV
broken: list[tuple[Block, str]] = []
with tempfile.TemporaryDirectory(prefix="diagrams-") as tmp:
workdir = Path(tmp)
config = workdir / "puppeteer.json"
config.write_text(json.dumps({"args": ["--no-sandbox"]}), encoding="utf-8")
for block in blocks:
error = render(cmd, block, workdir, config)
if error is not None:
broken.append((block, error))
files = len({b.path for b in blocks})
print(f"диаграмм {len(blocks)} в {files} файлах")
if broken:
print()
for block, error in broken:
print(f"НЕ РЕНДЕРИТСЯ {block.where}\n {error}")
print(f"\nИтог: не рендерятся {len(broken)}."
f" Блок правится в самом markdown — картинок в репозитории нет"
f" и не заводится, диаграмма живёт исходником рядом с текстом.")
return DRIFT
print("все рендерятся")
return OK
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(INTERNAL)
except Exception as e: # noqa: BLE001 — последний рубеж, код 4 по словарю
print(f"внутренний сбой ({type(e).__name__}): {e}", file=sys.stderr)
sys.exit(INTERNAL)