словарь, манифесты, README: одно слово — одна вещь, одно описание — один дом

- «готовность» значила и «запись можно брать», и «что считается сделанным»;
  второй смысл стал «определением сделанного» — своё же правило про занятое
  слово запрещало это прямо
- «пайплайн» жил в 24 местах вне журналов при том, что DECISIONS фиксирует
  его уход «целиком»; рабочее имя — конвейер
- «чекпоинт» в review значил стадию и проход, в resolve — остановку человеку;
  слово оставлено за остановкой
- у описания плагина было два дома, и три из четырёх уже разошлись. Сведены,
  и класс закрыт машиной: frontmatter.py сверяет plugin.json с marketplace,
  гейт разбужен на *.json
- README врал про односторонние зависимости и терял healthcheck на диаграмме
- перечень агентов в REMAINING отстал на два поколения

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-08-09 18:45:39 +03:00
co-authored by Claude Opus 5
parent 63ba36d71d
commit 12882911a9
22 changed files with 180 additions and 90 deletions
+54 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Проверка фронтматтеров скиллов и charter'ов этого репозитория.
"""Проверка фронтматтеров скиллов и charter'ов этого репозитория и описаний плагинов.
Фронтматтер — единственная часть скилла, которую читает не человек, а загрузчик:
по `name` он разрешает вызов, по `description` решает, звать ли скилл вообще.
@@ -7,7 +7,7 @@
разумную строку, а скилл либо не находится по имени, либо загружается с
обрезанным описанием и потому не срабатывает на своих же триггерах.
Ловится три класса.
Ловится четыре класса.
**Двоеточие с пробелом в описании без кавычек.** В YAML `: ` внутри простого
скаляра начинает вложенное отображение — строка «конвейер ревью: гейт, сверка…»
@@ -26,8 +26,14 @@
списку агентов, и держаться вниманием оно не может: цвет ставится один раз при
заведении charter'а, а модель потом меняется калибровкой.
**Описание плагина, разошедшееся между манифестами.** У описания два дома:
`<плагин>/.claude-plugin/plugin.json` его показывает установленному плагину,
корневой `.claude-plugin/marketplace.json` — тому, кто выбирает, ставить ли.
Правят обычно один, и разойтись они успели уже трижды из четырёх. `copies.py`
этот класс не берёт: он смотрит markdown, а манифест — json.
Коды выхода — тот же словарь, что у tasks.py, docs.py, copies.py и diagrams.py:
0 все фронтматтеры в порядке
0 все фронтматтеры и описания в порядке
1 расхождение
2 ошибка употребления: аргументы
3 окружение: не тот каталог
@@ -37,6 +43,7 @@
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
@@ -129,6 +136,40 @@ def collect(root: Path) -> list[tuple[Sheet, str, set[str]]]:
return found
def manifests(root: Path) -> list[tuple[str, list[str]]]:
"""Описание каждого плагина: `plugin.json` против `marketplace.json`."""
market = root / ".claude-plugin" / "marketplace.json"
where = market.relative_to(root).as_posix()
try:
listed = {
str(entry.get("name", "")): str(entry.get("description", ""))
for entry in json.loads(market.read_text(encoding="utf-8"))["plugins"]
}
except (OSError, ValueError, KeyError, TypeError) as e:
return [(where, [f"манифест маркетплейса не разбирается: {e}"])]
found: list[tuple[str, list[str]]] = []
for plugin in sorted(root.glob("av-*/")):
card = plugin / ".claude-plugin" / "plugin.json"
rel = card.relative_to(root).as_posix()
try:
own = json.loads(card.read_text(encoding="utf-8"))
except (OSError, ValueError) as e:
found.append((rel, [f"манифест плагина не разбирается: {e}"]))
continue
name = str(own.get("name", plugin.name))
if name not in listed:
found.append((rel, [
f"плагина `{name}` нет в {where} — маркетплейс его не отдаёт"
]))
elif str(own.get("description", "")) != listed[name]:
found.append((rel, [
f"`description` разошлось с записью `{name}` в {where}:"
f" у описания один текст на два манифеста, и правят обычно один"
]))
return found
def main() -> int:
ap = argparse.ArgumentParser(description="Проверка фронтматтеров.")
ap.add_argument("--dir", default=".", help="корень репозитория")
@@ -150,17 +191,25 @@ def main() -> int:
if sheet.parsed:
sheet.check(expected, required)
cards = manifests(root)
plugins = len(list(root.glob("av-*/")))
skills = sum(1 for _, _, required in sheets if required is SKILL_KEYS)
print(f"фронтматтеров {len(sheets)}: скиллов {skills},"
f" charter'ов {len(sheets) - skills}")
print(f"манифестов плагинов {plugins}: описание сверено с marketplace.json")
broken = [sheet for sheet, _, _ in sheets if sheet.problems]
if broken:
if broken or cards:
print()
for sheet in broken:
for problem in sheet.problems:
print(f"ОШИБКА {sheet.where}\n {problem}")
print(f"\nИтог: с ошибками {len(broken)} из {len(sheets)}.")
for rel, problems in cards:
for problem in problems:
print(f"ОШИБКА {rel}\n {problem}")
print(f"\nИтог: с ошибками {len(broken) + len(cards)}"
f" из {len(sheets) + plugins}.")
return DRIFT
print("все в порядке")