границы плагинов: путь в чужое дерево, безымянные стыки, звонящий у вычитки

Правило границы моё, копий восемь — и нарушал его я же.

- путь в дерево чужого плагина снят из пяти мест; маркер копии, уезжающий
  в проект скелетом, оставлен, но сказано, что сама пара маркеров не едет
- короткое имя чужого скилла в четырёх местах стало полным
- стык «урожай ревью → задачи» не был назван ни с одной стороны, хотя
  механика написана с обеих; теперь назван, с веткой «плагина нет»
- resolve звал av-dev-git:commit без строки доклада и пересказывал формат
  коммита, нарушая собственное «ссылайся, не пересказывай»
- doc-wording обещал момент вызова, которого не исполнял никто. Правило:
  звонящий — тот, кто только что писал текст. Вызов появился шагом в docs,
  init, adopt и upgrade; healthcheck по-прежнему его не зовёт
- openspec.py искал SHALL по всему файлу, а образец даёт его в context —
  проверка молчала ровно в том случае, ради которого написана
- фаза 2 review-rubric была недостижима; проход стал судить задуманное,
  а не код, и это сходится с тем, что о нём говорит конвейер
- rules.tasks в образце конфига, ветка «записи задачи нет» у review-scope,
  возвраты на чекпоинт в схеме resolve, старшинство правила дельта-спек

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-08-09 18:22:35 +03:00
co-authored by Claude Opus 5
parent 15dba79993
commit 63ba36d71d
20 changed files with 212 additions and 85 deletions
+10 -4
View File
@@ -78,11 +78,14 @@ python3 $os form # слепок формы против жив
Различать 1 и 3 обязательно: «форма разошлась» — рабочая ситуация, «openspec не
отвечает» — нерабочая.
`check` проверяет пять вещей, и каждая — про молчащий пробел, а не про вкус:
`check` проверяет форму, и каждая проверка — про молчащий пробел, а не про вкус:
каталог есть; имя именно `config.yaml` (`config.yml` OpenSpec не читает и об этом
не сообщает); `context` и `rules.specs` не остались примером, а правила называют
`SHALL`; `context` называет паспорт и `CLAUDE.md`; ключи под `rules:` — имена
артефактов схемы, а не свободные слова.
не сообщает); ключ `schema` называет ту схему, для которой форма описана;
`context` и `rules.specs` не остались примером, **а `SHALL` назван именно внутри
`rules.specs`** (в `context` он стоит и в образце, поэтому греп по файлу здесь
ничего не значит); `context` называет паспорт и `CLAUDE.md`; ключи под `rules:`
имена артефактов схемы, а не свободные слова. Числа проверок здесь нет намеренно:
оно протухает от каждой добавленной.
**Адреса требуются только к тем документам, которые в проекте есть.** Канон
документов ставится отдельным плагином и может быть не подключён; требовать
@@ -116,6 +119,9 @@ python3 $os form # слепок формы против жив
- `av-dev-docs:init` — шагом заведения нового проекта, до первого документа;
- `av-dev-docs:canon` в режиме `adopt` — если на переводимом проекте каталога нет
или `config.yaml` остался примером;
- `av-dev-code:resolve` и `av-dev-code:review` — не вызовом по ходу, а отсылкой:
OpenSpec у обоих жёсткая предпосылка, и на проекте без каталога оба посылают
сюда вместо того, чтобы заводить его руками;
- человек — когда конвейер отказался работать без источника требований.
**Копия.** Дом правила — `shared/plugin-boundary.md` в репозитории плагинов.
@@ -67,6 +67,10 @@ rules:
- "Сценарий — ровно #### (четыре решётки); три или список молча теряются"
- "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его"
- "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском"
tasks:
- "Критерии приёмки задачи — отдельным блоком и дословно: файл задачи закрытие удалит, критерии обязаны его пережить"
- "Рубрика ревью дизайна, если оно её дало, идёт в тот же блок: приёмка судится по одному списку, а не по двум"
- "Шаг плана формулируется проверяемо — по нему видно «сделано / не сделано» без суждения"
```
**Четыре правила для `specs` сняты отказами валидатора, а не выведены из
@@ -79,7 +83,14 @@ rules:
артефактов**, а не сочиняется заново: третий пересказ одного и того же разошёлся
бы с обоими. Требование поэтому стоит здесь, в момент порождения артефакта, а не
в скилле, который спохватится позже. `design.md` при этом ещё и **сырьё для
ADR** — отвергнутый вариант с названной причиной и есть половина будущей записи. Блок `context` проект
ADR** — отвергнутый вариант с названной причиной и есть половина будущей записи.
**Правила для `tasks` держит тот же скилл, и по той же причине — момент
порождения.** `tasks.md` — единственное, что переживает задачу: файл задачи
закрытие удаляет, а приёмка потом судится по критериям, которые в него
скопированы. Туда же ложится рубрика ревью дизайна, если оно её дало. Записанное
в момент порождения не приходится вспоминать шагом позже, когда артефакт уже
написан. Блок `context` проект
дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и
`CLAUDE.md` обязательны** — отсутствие адреса к существующему документу
`openspec.py check` называет отказом.
@@ -134,6 +134,35 @@ def rules_keys(live: str) -> list[str]:
return out
def rules_block(live: str, name: str) -> str:
"""Строки правил, адресованных одному артефакту.
Обход тот же, что у `rules_keys`, и по той же причине: искать по всему файлу
нельзя. Литеральный скаляр `context` называет `SHALL` уже в образце, поэтому
проверка «правила называют SHALL» грепом по файлу проходила при **пустом**
`rules.specs` — то есть молчала ровно в том случае, ради которого написана.
"""
out: list[str] = []
in_rules = False
in_name = False
for line in live.splitlines():
if not line.strip():
continue
if not line[0].isspace():
in_rules = line.startswith("rules:")
in_name = False
continue
if not in_rules:
continue
m = re.fullmatch(r" ([A-Za-z_-]+):\s*", line)
if m:
in_name = m.group(1) == name
continue
if in_name:
out.append(line)
return "\n".join(out)
def check_form(root: Path, rep: Report) -> None:
"""Настройка заведена и не осталась примером из коробки."""
os_dir = root / "openspec"
@@ -197,12 +226,12 @@ def check_form(root: Path, rep: Report) -> None:
if pointer not in live:
rep.error(f"openspec/config.yaml не называет {pointer}{why}")
if "rules" not in keys or "specs:" not in live:
if "rules" not in keys or "specs" not in rules_keys(live):
rep.error(
"в openspec/config.yaml нет rules.specs — придирки валидатора "
"нигде не записаны, и каждое предложение узнаёт их отказом"
)
elif "SHALL" not in live:
elif "SHALL" not in rules_block(live, "specs"):
rep.error(
"rules.specs в openspec/config.yaml не называет SHALL — "
"требование без этого литерала валидатор отвергает, а правило "