From bd1ea6d6b1469c35ec75b9c823fd74fa6308a89e Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Mon, 3 Aug 2026 20:41:42 +0300 Subject: [PATCH] =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D1=88=D0=B8=D0=BD?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D0=BE=20=D0=B4=D0=B8=D0=B0=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D0=BC=D0=BC=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE,=20=D1=80=D0=B5=D0=BD=D0=B4=D0=B5=D1=80=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Диаграмма и проза вокруг неё описывают один факт — это второй дом, и разойтись они могут молча: то самое, против чего написан 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) --- DECISIONS.md | 15 ++ README.md | 21 ++ .../skills/review-pipeline/SKILL.md | 4 + .../review-pipeline/references/calibration.md | 3 + .../review-pipeline/references/promote.md | 3 + av-dev-pipeline/skills/task-batch/SKILL.md | 4 + av-dev-pipeline/skills/task-pipeline/SKILL.md | 3 + av-dev-pm/skills/session/SKILL.md | 3 + av-dev-pm/skills/session/references/sprint.md | 3 + av-dev-pm/skills/tasks/SKILL.md | 3 + pyproject.toml | 1 + scripts/diagrams.py | 188 ++++++++++++++++++ 12 files changed, 251 insertions(+) create mode 100644 scripts/diagrams.py diff --git a/DECISIONS.md b/DECISIONS.md index d8b7d6f..40820a2 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1065,3 +1065,18 @@ HTML-комментарии, невидимые в отрендеренном ma 62. **Проход, держащий машину, знает об этом из своего charter'а.** `adversary` и `ops` получили по абзацу: цепочка гарантирует им чистое железо, значит их число — оракул, и шум в нём объясняется замером, а не соседом. +63. **У каждой диаграммы объявлено старшинство — это цена второго дома.** Схема + и проза вокруг неё описывают один факт, и разойтись они могут молча: то + самое, против чего написан `copies.py`. Механической сверки здесь нет — + дословного соответствия между текстом и графом не существует, — поэтому + работает объявление: **в `review-pipeline` старший граф** (он и есть алгоритм + планировщика, проза объясняет рёбра), **в остальных местах старшая проза** + (диаграмма там сводка). Для агента это не философия: без объявления он идёт + за тем, что конкретнее, то есть чаще за схемой. +64. **Рендер диаграмм проверяется скриптом, а не памятью автора.** + `scripts/diagrams.py` вынимает все блоки `mermaid` и гонит их через + `mmdc` или `npx @mermaid-js/mermaid-cli`; коды выхода — общий словарь, нет + рендерера — код 3, а не молчаливый успех. Причина та же, что у остальных + проверок репозитория: **ошибка в блоке не видна при чтении** — текст + правдоподобен, дифф разумен, падает только рендер. Расхождение с прозой + скрипт не ловит и не притворяется, что ловит: это работа правила 63. diff --git a/README.md b/README.md index 1d94cb4..da27e58 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ openspec/ /skills//references/ что читается по ссылке из скилла /skills//scripts/ tasks.py, docs.py /agents/ charter'ы сабагентов +scripts/ проверки самого репозитория: копии, диаграммы pyproject.toml линтеры скриптов, только для этого репозитория ``` @@ -197,3 +198,23 @@ uv run python scripts/copies.py # 0 сошлось, 1 расхождение Скрипт ловит четыре вещи: копия разошлась с домом (с диффом), копия указывает не на тот файл, дом остался без копий, разметка сломана. Чего он **не** ловит — копию, которую забыли пометить: помечать — по-прежнему решение человека. + +## Проверка диаграмм + +Диаграммы `mermaid` живут исходником в markdown — картинок в репозитории нет. +Синтаксическая ошибка в блоке **не видна при чтении**: текст выглядит +правдоподобно, диff показывает разумную строку, а рендер падает. + +``` +uv run python scripts/diagrams.py # 0 рендерятся, 1 нет, 3 нет mermaid-cli +``` + +Рендерит `mmdc` с PATH или `npx --yes @mermaid-js/mermaid-cli`; ни того ни +другого нет — код 3, а не молчаливый успех. Прогон занимает секунды на блок, +поэтому он не в гейте, а в руках того, кто правит диаграмму. + +Чего проверка **не** ловит — расхождение диаграммы с прозой вокруг неё. Дословного +соответствия между текстом и графом нет, сличать нечего, и держится это +правилом старшинства, записанным рядом с каждой диаграммой: **в конвейере ревью +старший граф** (он и есть алгоритм планировщика, проза его объясняет), **в +остальных местах старшая проза** (диаграмма там сводка). diff --git a/av-dev-pipeline/skills/review-pipeline/SKILL.md b/av-dev-pipeline/skills/review-pipeline/SKILL.md index 2d7f2ba..80ee52f 100644 --- a/av-dev-pipeline/skills/review-pipeline/SKILL.md +++ b/av-dev-pipeline/skills/review-pipeline/SKILL.md @@ -226,6 +226,10 @@ flowchart TD `code` и первый из меряющей пары, — а второй меряющий идёт следом за первым. В `quick` — `specs` и `code` разом, и сразу триаж. +**Схема здесь старше прозы.** Она не иллюстрация к тексту, а сам алгоритм +планировщика; проза ниже объясняет рёбра и называет их цену. Разошлись — прав +граф, а расхождение чинится правкой текста. + **Ребро значит «A закончил раньше, чем B стартовал», и ничего больше.** В обычном графе задач ребро тянет за собой данные — здесь нет, и это не деталь реализации. Проход **не видит** находок других проходов, в каком бы порядке их ни запустили. diff --git a/av-dev-pipeline/skills/review-pipeline/references/calibration.md b/av-dev-pipeline/skills/review-pipeline/references/calibration.md index 63b258c..8ec9619 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/calibration.md +++ b/av-dev-pipeline/skills/review-pipeline/references/calibration.md @@ -40,6 +40,9 @@ stateDiagram-v2 r2 --> dead: снова не ловит — это театр ``` +Схема — **сводка** к таблице вердиктов выше: она добавляет только счётчик, и при +расхождении прав таблица. + **`retune` не более двух раз подряд.** Проход, не находящий дефект своего класса в 2 из 3 прогонов после двух правок промпта, — это театр. Удалять, а не бесконечно править формулировки: каждая итерация правки промпта стоит дороже, diff --git a/av-dev-pipeline/skills/review-pipeline/references/promote.md b/av-dev-pipeline/skills/review-pipeline/references/promote.md index 9b84354..2de02a1 100644 --- a/av-dev-pipeline/skills/review-pipeline/references/promote.md +++ b/av-dev-pipeline/skills/review-pipeline/references/promote.md @@ -32,6 +32,9 @@ flowchart TD чаще, чем ловит, снимается в прозу. Ребро `rule → clean` **обязательное**: без него первые два шага не окупаются, а именно его и пропускают. +Схема — **сводка**: условия каждого шага в его разделе, и при расхождении прав +текст. + ## Шаг 1. Находка → конвенция Условия: находка **принята** при ревью (не отвергнута, не понижена в гипотезу) и diff --git a/av-dev-pipeline/skills/task-batch/SKILL.md b/av-dev-pipeline/skills/task-batch/SKILL.md index ec73977..03852c4 100644 --- a/av-dev-pipeline/skills/task-batch/SKILL.md +++ b/av-dev-pipeline/skills/task-batch/SKILL.md @@ -176,6 +176,10 @@ flowchart TD линеаризации законны); **параллельно** — волна 1 `A` одна (замеряющая), волна 2 `B` и `C`, волна 3 `D`. +Схемы в этом скилле — **пример и сводка**, правила ставит текст: при расхождении +прав он. (В `av-dev-pipeline:review-pipeline` наоборот — там граф прогона и есть +алгоритм, и старший он.) + Покажи план короткой репликой — режим, порядок или состав волн, какие задачи признаны замеряющими и по какому триггеру, — и иди дальше. diff --git a/av-dev-pipeline/skills/task-pipeline/SKILL.md b/av-dev-pipeline/skills/task-pipeline/SKILL.md index 9dd805a..e05d0b8 100644 --- a/av-dev-pipeline/skills/task-pipeline/SKILL.md +++ b/av-dev-pipeline/skills/task-pipeline/SKILL.md @@ -178,6 +178,9 @@ flowchart TD порядок «сперва коммит работы, потом коммит учёта» на схеме тоже ребро, и оно обязательное (шаг 11). +Схема — **сводка**: содержание каждого шага в его разделе ниже, и при +расхождении прав текст. + ### 1. Прочитать задачу Задача задана извне (slug, файл, ссылка, описание) — прочитай её и связанные diff --git a/av-dev-pm/skills/session/SKILL.md b/av-dev-pm/skills/session/SKILL.md index 5da0426..812f181 100644 --- a/av-dev-pm/skills/session/SKILL.md +++ b/av-dev-pm/skills/session/SKILL.md @@ -140,6 +140,9 @@ flowchart TD s4 --> sprint ``` +Схема — **сводка**: процедура каждого шага в +[references/cadence.md](references/cadence.md), и при расхождении прав текст. + Процедура каждого шага, размер и отбор порции, храповик на залежавшихся, формат интерактива и доклад — [references/cadence.md](references/cadence.md). diff --git a/av-dev-pm/skills/session/references/sprint.md b/av-dev-pm/skills/session/references/sprint.md index a3a49d4..538dc5a 100644 --- a/av-dev-pm/skills/session/references/sprint.md +++ b/av-dev-pm/skills/session/references/sprint.md @@ -59,6 +59,9 @@ flowchart TD `sprint close`** (после команды автотег уже не поставится) и **блокер в обход исходов** (спринт распускается, а не ждёт). +Схема — **сводка**: определение готовности и правила приёмки ниже, и при +расхождении прав текст. + **Урожай заводится при закрытии спринта, а не при закрытии задачи.** Это обязанность закрывающего: пройти по спискам находок от исполнителей и завести недостающее интейком скилла `tasks` — с дедупликацией и картой человеку. Заводимое diff --git a/av-dev-pm/skills/tasks/SKILL.md b/av-dev-pm/skills/tasks/SKILL.md index 1b4cfae..d539a7c 100644 --- a/av-dev-pm/skills/tasks/SKILL.md +++ b/av-dev-pm/skills/tasks/SKILL.md @@ -105,6 +105,9 @@ stateDiagram-v2 нет намеренно — каждый переход это команда, и другого способа его совершить не существует. +Схема — **сводка**: условия и оговорки живут в тексте разделов, и при +расхождении прав текст. + ## Цели **Цель — такой же файл в `items/`, тип `[goal]`**, перечисленный в `PLAN.md`: diff --git a/pyproject.toml b/pyproject.toml index 18930e5..7388bcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,5 +62,6 @@ project-includes = [ "av-dev-pm/skills/tasks/scripts/tasks.py", "av-dev-pm/skills/canon/scripts/docs.py", "scripts/copies.py", + "scripts/diagrams.py", ] python-version = "3.12" diff --git a/scripts/diagrams.py b/scripts/diagrams.py new file mode 100644 index 0000000..1ff631b --- /dev/null +++ b/scripts/diagrams.py @@ -0,0 +1,188 @@ +#!/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)