словарь, манифесты, 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:
+54
-5
@@ -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("все в порядке")
|
||||
|
||||
Reference in New Issue
Block a user