diff --git a/DECISIONS.md b/DECISIONS.md index d0ba774..8d40bed 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -3031,3 +3031,55 @@ JJJ): у профиля обязан быть один правильный от режется, вложенное (метки) — нет: разрез вложенного даёт дублирование общей части, а дублирование намеренно неточное машина не сверит. + +## 47. OpenSpec заводится скиллом, а его конфиг — часть канона (2026-08-07) + +**АЕАИА. `init` заводит OpenSpec сам, а не оставляет это человеку.** Каталог +`openspec/` был предпосылкой, о которой канон говорил, но за которой не следил: +`openspec/specs/` объявлен домом темы `requirements`, `config.yaml` описан +абзацем — а заводилось всё руками, и не проверялось ничего. Новый проект выходил +из `init` с полным каноном документов и без каталога, без которого не работают ни +`opsx:propose`, ни ревью дизайна, ни сверка требований. Команда названа поимённо +(`openspec init --tools claude`) в трёх местах — скилле, каноне и отказе скрипта: +отказ без команды заставляет искать её в другом месте. + +**АЕАИБ. Файл из коробки хуже отсутствующего, и потому проверяется машиной.** +`openspec init` кладёт `config.yaml`, где `context` и `rules` — закомментированный +пример на английском. Такой файл читается как настроенный: он есть, он валиден, +имя правильное. Работает он как пустой, и узнаётся это по уже написанному +предложению — на другом языке, с capability по имени пакета, без единого `SHALL`. +`docs.py` проверяет четыре вещи, и каждая про молчащий пробел: каталог есть; имя +именно `config.yaml` (`config.yml` OpenSpec не читает и об этом не сообщает); +`context` и `rules.specs` не остались примером, а правила называют `SHALL`; +`context` называет `passport` и `CLAUDE.md`. + +**АЕАИВ. Форма конфига — маршрутизатор, и это разрез, а не пожелание.** +Утверждение, которое можно опровергнуть, открыв другой файл проекта, — пересказ; +строка, которая говорит, какой файл открыть, — ссылка. `context` читается при +порождении **каждого** артефакта, туда удобно дописать «чтобы агент знал», и +именно поэтому в нём заводятся вторые дома инвариантов, конвенций, гейта и правил +ревью. Машина этот разрез не проверяет — отличить ссылку от пересказа она не +умеет, — и он отдан `doc-consistency` отдельным абзацем правила «один факт — один +дом», с `config.yaml`, добавленным ему во вход. + +**Обязательными сделаны ровно два адреса — паспорт и `CLAUDE.md`.** Причина в +порядке работы: предложение пишется **до** того, как кто-либо откроет `docs/`, и +без этих двух строк его пишут, не зная ни границы домена, ни инвариантов. +Длинный список адресов превратил бы `context` во второй дом ровно тем способом, +против которого правило и заведено. + +**Образец конфига лёг в канон, а не в конвейер**, как планировалось решением C. +Форма документа принадлежит тому, кто владеет каноном документов; конвейер её +читатель. Иначе `av-dev-pipeline` завёл бы у себя описание файла, который заводит +и проверяет `av-dev-pm`, — тот же шов, что разбирали, убирая имена проходов из +канона. + +### Что из этого следует + +167. **Предпосылка, за которой никто не следит, — не предпосылка, а пожелание.** + Если условие названо обязательным, его должен кто-то заводить и кто-то + проверять; иначе оно живёт ровно до первого проекта, где о нём забыли. +168. **Заполненная форма и заполненный смысл — разные вещи, и первая маскирует + вторую.** Файл на месте, валиден, с правильным именем — и пуст по существу: + это худший вид пробела, потому что выглядит он как его отсутствие. + diff --git a/av-dev-pipeline/skills/review-pipeline/SKILL.md b/av-dev-pipeline/skills/review-pipeline/SKILL.md index 9178449..02c6a1f 100644 --- a/av-dev-pipeline/skills/review-pipeline/SKILL.md +++ b/av-dev-pipeline/skills/review-pipeline/SKILL.md @@ -51,7 +51,9 @@ description: "Конвейер ревью изменения, устроенны упадут на «нет такого скилла», а `review-specs` останется без источника требований. **Проект без OpenSpec этим конвейером не проверяется** — подключай OpenSpec, а не понижай прогон: ветка деградации здесь не пишется, потому что - непроверенная ветка деградации хуже честного отказа. + непроверенная ветка деградации хуже честного отказа. Заводить руками не надо: + `av-dev-pm:init` делает `openspec init` на новом проекте, `canon adopt` — на + переводимом, и оба кладут `openspec/config.yaml` канонической формы. - **Документы канона** — см. следующий раздел. - **Проектные копии этих скиллов и агентов удаляются при установке.** Если в проекте уже лежат свои `.claude/skills/review-pipeline`, diff --git a/av-dev-pm/agents/doc-consistency.md b/av-dev-pm/agents/doc-consistency.md index be4888a..ffd014a 100644 --- a/av-dev-pm/agents/doc-consistency.md +++ b/av-dev-pm/agents/doc-consistency.md @@ -46,8 +46,9 @@ color: yellow ## Что тебе дают Корень проекта. Твоё чтение — `docs/**` (кроме `docs/tasks/`, его ведёт -`tasks.py`), `CLAUDE.md` и `openspec/specs/**`. Плюс `openspec/changes/archive/`, -когда проверяешь ADR: там лежат `design.md`, из которых записи промоутятся. +`tasks.py`), `CLAUDE.md`, `openspec/specs/**` и `openspec/config.yaml`. Плюс +`openspec/changes/archive/`, когда проверяешь ADR: там лежат `design.md`, из +которых записи промоутятся. **Кода ты не читаешь.** Разошёлся ли документ с кодом — вопрос агента `doc-code-drift`, и у него для этого другой вход и другая цена. @@ -63,6 +64,17 @@ color: yellow важнее совпадающих: совпадающие разойдутся завтра, разошедшиеся уже врут, и в этом случае назови **оба значения**, не выбирая за человека. + **Самое частое место второго дома — блок `context` в `openspec/config.yaml`.** + Он читается при порождении каждого артефакта, туда удобно дописать «чтобы + агент знал», и так в нём заводятся инварианты, перечень конвенций, состав + шагов гейта, границы домена и правила ревью. По канону там законны только + нужды порождения — язык, именование capability, придирки валидатора — и + **адреса** документов. Разрез проверяемый: **утверждение, которое можно + опровергнуть, открыв другой файл проекта, — пересказ и находка; строка, + которая говорит, какой файл открыть, — ссылка и норма.** Форму `config.yaml` + машина проверяет, этот разрез — нет: отличить ссылку от пересказа она не + умеет, и потому он твой. + 2. **Прямое противоречие между документами.** Самое дорогое, что ты находишь, и искать его надо адресно, а не вычитыванием подряд. Пары, которые расходятся чаще прочих: diff --git a/av-dev-pm/skills/canon/SKILL.md b/av-dev-pm/skills/canon/SKILL.md index c8db5e0..9162830 100644 --- a/av-dev-pm/skills/canon/SKILL.md +++ b/av-dev-pm/skills/canon/SKILL.md @@ -136,20 +136,25 @@ capability), `openspec/config.yaml`. 1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть; 2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**: незаполненное — одной честной информативной строкой, а не «TBD»; -3. переносы содержимого; -4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он +3. **OpenSpec, если его нет** — `openspec init --tools claude`, и `config.yaml` + по тому же скелету. Каталог есть, а `config.yaml` из коробки — тот же случай, + что отсутствие: закомментированный пример выглядит настройкой и не является + ею. Пересказ инвариантов, конвенций и правил ревью из `context` вычисти + ссылкой на дом — на переводимом проекте он там почти наверняка есть; +4. переносы содержимого; +5. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он владеет форматом задач. Он же переименует транслитные слаги в английские и тем же проходом починит перекрёстные ссылки; -5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`, +6. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`, `CLAUDE.md`, `README.md`; -6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**; -7. **шаг `docs.py check` в гейт проекта.** Путь к скрипту — переменной с +7. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**; +8. **шаг `docs.py check` в гейт проекта.** Путь к скрипту — переменной с умолчанием на канонический путь маркетплейса, чтобы переустановка плагина не меняла `Taskfile`; шаг обязан **краснеть внятно**, если скрипт не найден, а не пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе. Пример строки покажи человеку — гейт принадлежит проекту, и правит его он; -8. `docs.py check` — до **отсутствия дрейфа раскладки**. Замечания +9. `docs.py check` — до **отсутствия дрейфа раскладки**. Замечания (незаполненные плейсхолдеры, слабое упоминание capability) остаются: незаполненный канон это объявленное переходное состояние из шага 5, а не отказ. **Пункт «задачи без цели» из вложенной проверки `tasks.py` тоже diff --git a/av-dev-pm/skills/canon/references/canon.md b/av-dev-pm/skills/canon/references/canon.md index d9552e5..3ee6dc6 100644 --- a/av-dev-pm/skills/canon/references/canon.md +++ b/av-dev-pm/skills/canon/references/canon.md @@ -1,6 +1,6 @@ # Канон документов проекта -**Версия 6.** +**Версия 7.** Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs` читают его, а не пересказывают: три описания одной раскладки разъедутся, и @@ -106,6 +106,7 @@ openspec/ | `database.*` | источник | `operations` — схема и настройки с числами | | `CLAUDE.md`, `AGENTS.md` | источник | `autotests` (семантика гейта); инварианты — сквозные | | `openspec/specs/` | источник | `requirements` | +| `openspec/config.yaml` | процессный | — (настройка порождения артефактов, слой **до** тем) | | `tasks/` | процессный | — | | `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) | | `adr.*` | процессный | — | @@ -411,10 +412,40 @@ kebab-case.** Причина не эстетическая: имя файла с ### `openspec/config.yaml` **Только нужды генерации артефактов** — язык, правила именования capability, -придирки валидатора RFC 2119 — плюс ссылки на документы канона. Правило ревью, +придирки валидатора RFC 2119 — плюс **адреса** документов канона. Правило ревью, пересказ конвенций и инварианты сюда не пишутся: у них есть свои дома, и второй дом разойдётся на первой же правке. +**Каталог `openspec/` — часть канона, а не соседняя технология.** В нём дом темы +`requirements`, и заводится он командой: `openspec init --tools claude`. Её +выполняет `init` на новом проекте и `adopt` на переводимом; из канона она названа +поимённо потому, что её печатает отказ `docs.py`, а отказ без команды заставляет +искать её в другом месте. + +**Файл из коробки настройкой не является.** `openspec init` кладёт `config.yaml`, +где и `context`, и `rules` лежат закомментированным примером. Такой файл читается +как настроенный — он есть, он валиден, у него правильное имя, — а работает как +пустой: предложение пишется без языка, без правил именования capability и без +знания, где лежит граница домена. Это ровно тот класс, против которого написан +весь канон, и потому здесь он проверяется машиной, а не чтением. + +Проверяется четыре вещи, и каждая — про молчащий пробел, а не про вкус: + +1. **`openspec/` есть.** Нет — нет и дома темы `requirements`. +2. **Имя файла `config.yaml`.** `config.yml` OpenSpec не читает и об этом не + сообщает: настройка, написанная в файл с таким именем, пропадает целиком. +3. **`context` и `rules.specs` не остались примером.** Правила для `specs` + обязаны называть `SHALL`: требование без этого литерала валидатор отвергает. +4. **`context` называет `passport` и `CLAUDE.md`.** Предложение пишется **до** + того, как кто-либо откроет `docs/`; без этих двух адресов его пишут, не зная + ни границы домена, ни инвариантов. + +Пятого — «нет ли здесь пересказа» — машина не проверяет: отличить ссылку от +пересказа она не умеет. Это работа `doc-consistency`, и раздел «Что проверяет +машина, а что человек» называет её строкой. + +Форма — [skeletons.md](skeletons.md). + ## Правило единственного дома Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора: @@ -482,6 +513,7 @@ kebab-case.** Причина не эстетическая: имя файла с | маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` | | миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` | | capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` | +| `openspec/config.yaml`: имя, `schema`, незаменённый пример, адреса паспорта и `CLAUDE.md` | **пересказ документа канона в `context` вместо ссылки** | `doc-consistency` | | | связность и читаемость | `doc-wording` | **Агентов двое, и разведены они по глубине, а не по охвату.** `doc-consistency` @@ -507,7 +539,7 @@ kebab-case.** Причина не эстетическая: имя файла с ```json { - "canon": 6, + "canon": 7, "migrations": "internal/store/migrations", "tasks": { "backlog": "INDEX.md" diff --git a/av-dev-pm/skills/canon/references/changelog.md b/av-dev-pm/skills/canon/references/changelog.md index 41e700c..64c5d84 100644 --- a/av-dev-pm/skills/canon/references/changelog.md +++ b/av-dev-pm/skills/canon/references/changelog.md @@ -13,6 +13,62 @@ upgrade` идёт по записям снизу вверх от версии п --- +## Версия 7 — 2026-08-07 + +`openspec/` был предпосылкой, о которой канон говорил, но за которой не следил. +Каталог назван в раскладке, `openspec/specs/` объявлен домом темы `requirements`, +`config.yaml` описан абзацем — а заводил всё это человек руками, и проверялось +из перечисленного ничего. Заведение нового проекта проходило мимо: `init` +собирал документы канона и оставлял проект без каталога, без которого не работают +ни `opsx:propose`, ни ревью дизайна, ни сверка требований. + +Хуже отсутствия оказался файл из коробки. `openspec init` кладёт `config.yaml`, +где `context` и `rules` — закомментированный пример на английском. Такой файл +читается как настроенный: он есть, он валиден, имя правильное. Работает он как +пустой, и узнаётся это по предложению, написанному на другом языке, с +capability по имени пакета и без единого `SHALL`. + +**Что изменилось:** + +1. **`init` заводит OpenSpec сам** — `openspec init --tools claude`, до первого + документа канона. Команда названа в каноне поимённо, потому что её печатает + отказ `docs.py`. +2. **У `openspec/config.yaml` появилась каноническая форма** и скелет в + `skeletons.md`. Содержание — только то, что нужно **в момент порождения + артефакта**: язык, правила именования capability, придирки валидатора и + **адреса** документов канона. Пересказ паспорта, инвариантов, конвенций и + правил ревью в него не переносится. +3. **`docs.py check` проверяет четыре вещи:** каталог `openspec/` есть; файл + называется `config.yaml` (`config.yml` OpenSpec читать не станет и об этом не + сообщит); `context` и `rules.specs` не остались примером, а правила для + `specs` называют `SHALL`; `context` называет `passport` и `CLAUDE.md`. +4. **Пятое проверяет агент.** Отличить ссылку на документ от пересказа документа + машина не умеет — это работа `doc-consistency`, и в таблице «Что проверяет + машина, а что человек» она стоит строкой. + +**Что переехало:** ничего в раскладке `docs/`. Ни один файл не переименовывается +и не перемещается. + +**Что сделать проекту:** + +1. Нет `openspec/` — завести: `openspec init --tools claude`. Команда кладёт ещё + и `.claude/skills/openspec-*` с `.claude/commands/opsx/*`; это её нормальная + работа, удалять их не надо. +2. Открыть `openspec/config.yaml` и привести к скелету из + [skeletons.md](skeletons.md): блок `context` с языком, правилами именования + capability, требованием `SHALL` и **адресами** `docs/passport.md` и + `CLAUDE.md`; блок `rules` с четырьмя правилами для `specs`. +3. **Вычистить из `context` пересказ.** Инварианты, перечень конвенций, состав + шагов гейта, правило выбора метки и состав проходов ревью — заменить ссылкой + на дом. Признак пересказа простой: строку можно опровергнуть, открыв другой + файл проекта. +4. Проверить имя файла: `config.yml` переименовать в `config.yaml`. Если жили оба + — содержимое `.yml` до сих пор не читалось никем, и переносить из него нужно + именно то, чего нет в `.yaml`. +5. `docs/.pm.json`: `"canon": 7`. + +--- + ## Версия 6 — 2026-08-07 Версия 5 объявила: **каждый документ `docs/` — тема ревью**. Правило оказалось diff --git a/av-dev-pm/skills/canon/references/skeletons.md b/av-dev-pm/skills/canon/references/skeletons.md index da78ad9..d0f7cd1 100644 --- a/av-dev-pm/skills/canon/references/skeletons.md +++ b/av-dev-pm/skills/canon/references/skeletons.md @@ -417,11 +417,84 @@ severity стоит здесь, а не выводится каждым прох тронуть рабочие данные, без третьего вся шкала ранжирования триажа держится на догадке. +## `openspec/config.yaml` + +Каталог `openspec/` заводится командой — `openspec init --tools claude`, — и она +кладёт `config.yaml` с закомментированным примером внутри. Пример **заменяется +целиком**: нетронутый файл выглядит настроенным, а работает как пустой. + +**Это маршрутизатор, а не второй дом фактов.** Сюда пишут ровно то, что нужно +**в момент порождения артефакта** и чего в этот момент ещё никто не открыл: +язык, правила именования capability, придирки валидатора и **адреса** документов +канона. Пересказ паспорта, инвариантов, конвенций и правил ревью сюда не +переносится: расходится он молча, а замечают это в уже написанном предложении. + +```yaml +schema: spec-driven + +context: | + Language: Russian + Пиши на русском, но: + - Структурные заголовки оставляй на английском: + ## ADDED/MODIFIED/REMOVED Requirements, ### Requirement:, #### Scenario: + - Ключевые слова GIVEN/WHEN/THEN и RFC 2119 (SHALL/MUST/SHOULD) — на английском + - Технические термины, пути и код — на английском + + Имена capabilities: + - Capability — это ПОВЕДЕНИЕ или домен системы, а не пакет кода (совпадение с + именем пакета допустимо, но не критерий). + - Существительное, понятное без знания кода: ingest, parsing, storage, + read-api. НЕ store/httpapi — это реализация. + - Гранулярность по принципу «требования меняются вместе». Дробить, когда в + одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED + Requirements) — не дроби преждевременно в маленьком проекте. + + RFC 2119 — требование валидатора, не стиль: + - Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе + `openspec validate` падает. Поэтому эти слова и WHEN/THEN не русифицируем. + + Что это за проект — читай перед предложением, а не отсюда: + - docs/passport.md — цель, её граница (чем проект НЕ является), потребители, + типовые сценарии, референсы; + - CLAUDE.md — инварианты с severity и семантика гейта; + - docs/architecture.md — устройство; docs/security.md — периметр; + docs/adr/ — почему решено так; docs/research/ — что уже измерено. + Пересказа этих документов здесь нет намеренно: второй дом факта расходится с + первым молча, и заметно это становится в предложении, которое уже написано. + + Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом + скилл av-dev-pipeline:review-pipeline, проектная настройка — docs/review.md. + + Конвенции кода: механизированное проверяет гейт, прозой остаётся + docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не + пересказываем: и то и другое растёт по ходу задач. + + Развилка или блокер — сперва prior art. Готовые решения смотрим в референсах + паспорта, отвергаем — с названной причиной, и причина идёт в design.md этого + же изменения. + +rules: + proposal: + - Capabilities называй по поведению или домену системы, не по пакету кода + specs: + # Кавычки обязательны: без них YAML обрежет строку на первом '#'. + - "Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает)" + - "Сценарий — ровно #### (четыре решётки); три или список молча теряются" + - "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его" + - "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском" +``` + +**Четыре правила для `specs` сняты отказами валидатора, а не выведены из +документации** — потому и записаны дословно: без них каждое второе предложение +узнаёт их падением `openspec validate --strict`. Блок `context` проект +дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и +`CLAUDE.md` обязательны** — их отсутствие `docs.py check` называет отказом. + ## `docs/.pm.json` ```json { - "canon": 6 + "canon": 7 } ``` diff --git a/av-dev-pm/skills/canon/scripts/docs.py b/av-dev-pm/skills/canon/scripts/docs.py index e0d90aa..7bdac7c 100644 --- a/av-dev-pm/skills/canon/scripts/docs.py +++ b/av-dev-pm/skills/canon/scripts/docs.py @@ -25,7 +25,7 @@ from dataclasses import dataclass, field from pathlib import Path from typing import NoReturn -CANON_VERSION = 6 +CANON_VERSION = 7 OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4 @@ -76,6 +76,19 @@ DOC_EXTRA = { "adr": {"template.md": "шаблон записи ADR"}, } +# Настройка OpenSpec. Команда заведения — она же в скилле init; здесь потому, +# что её печатает отказ, а отказ без команды заставляет искать её в другом месте. +OPENSPEC_INIT = "openspec init --tools claude" + +# Адреса, которые обязан назвать блок context. Не пересказ документов, а именно +# ссылки: предложение пишется до того, как кто-либо откроет docs/, и без этих +# двух строк его пишут, не зная ни границы домена, ни инвариантов. Список +# короткий намеренно — длинный превращает context во второй дом фактов. +OPENSPEC_POINTERS = [ + ("passport", "граница домена и «чем НЕ является» останутся непрочитанными"), + ("CLAUDE.md", "инварианты и семантика гейта останутся непрочитанными"), +] + # Служебное в docs/ и каталог, который ведёт tasks.py. Оба процессные, но # проверок формы у них нет: .pm.json не markdown, tasks/ ведёт другой скрипт. NOT_DOCS = {".pm.json", "tasks"} @@ -455,6 +468,78 @@ def doc_text(root: Path, name: str) -> str | None: ) +def check_openspec(root: Path, rep: Report) -> None: + """Настройка OpenSpec заведена и не осталась примером из коробки. + + Разбираем текстом, а не YAML-парсером: у скриптов канона ноль внешних + зависимостей, а PyYAML в стандартной библиотеке нет. Всё, что проверяется + ниже, различимо построчно, и ложных срабатываний это не даёт: комментарии + отброшены, ключи верхнего уровня стоят в первой колонке. + """ + os_dir = root / "openspec" + if not os_dir.is_dir(): + rep.error( + "нет openspec/ — там дом темы requirements (openspec/specs/) и " + f"настройка генерации артефактов; заводится `{OPENSPEC_INIT}`" + ) + return + + if (os_dir / "config.yml").is_file(): + rep.error( + "openspec/config.yml — читается только config.yaml, и этот файл " + "останется незамеченным: настройка будет пустой, а выглядеть будет " + "заполненной" + ) + + path = os_dir / "config.yaml" + if not path.is_file(): + rep.error( + "нет openspec/config.yaml — язык, правила именования capability и " + "придирки валидатора будут заново угадываться на каждом предложении" + ) + return + + text = path.read_text(encoding="utf-8") + live = "\n".join( + line for line in text.splitlines() if not line.lstrip().startswith("#") + ) + keys = set(re.findall(r"(?m)^([A-Za-z_]+):", live)) + + schema = re.search(r"(?m)^schema:\s*(\S+)", live) + if schema is None: + rep.error("в openspec/config.yaml нет ключа schema — ожидается spec-driven") + elif schema.group(1) != "spec-driven": + rep.error( + f"schema в openspec/config.yaml — {schema.group(1)}, а канон описан " + f"для spec-driven" + ) + + if "context" not in keys: + rep.error( + "в openspec/config.yaml нет ключа context: файл остался примером из " + "коробки — предложение пишется без языка, правил именования " + "capability и адресов документов проекта" + ) + else: + for pointer, why in OPENSPEC_POINTERS: + if pointer not in live: + rep.error( + f"openspec/config.yaml не называет {pointer} — {why}" + ) + + if "rules" not in keys or "specs:" not in live: + rep.error( + "в openspec/config.yaml нет rules.specs — придирки валидатора " + "нигде не записаны, и каждое предложение узнаёт их отказом" + ) + elif "SHALL" not in live: + rep.error( + "rules.specs в openspec/config.yaml не называет SHALL — " + "требование без этого литерала валидатор отвергает, а правило " + "проекта об этом молчит" + ) + + def check_capabilities(root: Path, rep: Report) -> None: specs = root / "openspec" / "specs" text = doc_text(root, "architecture") @@ -588,10 +673,12 @@ def report(rep: Report) -> int: print(f" {msg}") print( - "\nМашина проверила раскладку, имена файлов, ссылки, версию и две сверки\n" - "с кодом. Согласованность документов между собой и с кодом она не\n" - "проверяет — это суждение агентов `doc-consistency` (документ ↔ документ\n" - "↔ openspec) и `doc-code-drift` (документ ↔ код)." + "\nМашина проверила раскладку, имена файлов, ссылки, версию, форму\n" + "openspec/config.yaml и две сверки с кодом. Согласованность документов\n" + "между собой и с кодом она не проверяет — как и то, ссылается ли\n" + "config.yaml на документы или пересказывает их. Это суждение агентов\n" + "`doc-consistency` (документ ↔ документ ↔ openspec) и `doc-code-drift`\n" + "(документ ↔ код)." ) if rep.errors: print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.") @@ -615,6 +702,7 @@ def cmd_check(args: argparse.Namespace) -> int: check_slugs(root, rep) check_links(root, rep) check_placeholders_and_debt(root, rep) + check_openspec(root, rep) check_capabilities(root, rep) check_migrations(root, cfg, args.base, rep) check_tasks(root, rep) diff --git a/av-dev-pm/skills/init/SKILL.md b/av-dev-pm/skills/init/SKILL.md index 0c74799..53ed9b7 100644 --- a/av-dev-pm/skills/init/SKILL.md +++ b/av-dev-pm/skills/init/SKILL.md @@ -1,6 +1,6 @@ --- name: init -description: "Завести новый проект — сессия вопросов и ответов по свободному описанию замысла, из которой рождается первичная документация по канону av-dev: паспорт, CLAUDE.md с инвариантами и командами, модель угроз с периметром, первые цели в роадмапе и скелет остальных документов. Использовать, когда начинают новый проект с нуля, когда есть только текст «что мне нужно и почему» и надо превратить его в рабочую документацию, когда просят провести стартовое интервью по брифу. Проект, где документация уже как-то ведётся, переводит скилл canon." +description: "Завести новый проект — сессия вопросов и ответов по свободному описанию замысла, из которой рождается первичная документация по канону av-dev: паспорт, CLAUDE.md с инвариантами и командами, модель угроз с периметром, первые цели в роадмапе и скелет остальных документов. Заводит и OpenSpec (openspec init) с настроенным openspec/config.yaml — дом темы requirements, без которого не работают ни propose, ни ревью. Использовать, когда начинают новый проект с нуля, когда есть только текст «что мне нужно и почему» и надо превратить его в рабочую документацию, когда просят провести стартовое интервью по брифу. Проект, где документация уже как-то ведётся, переводит скилл canon." --- # Заведение нового проекта @@ -28,6 +28,7 @@ description: "Завести новый проект — сессия вопро | `security.md` | `conventions/` | | `docs/tasks/ROADMAP.md` — первые цели | `research/`, `adr/` | | `docs/.pm.json` | `review.md` — журнал пуст, настройка появится с первым ревью | +| `openspec/config.yaml` | | Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет, заводится первой задачей». Проход читает её как факт. @@ -68,18 +69,30 @@ description: "Завести новый проект — сессия вопро 1. Прочитай бриф целиком. Выпиши, на какие блоки интервью ответ уже есть. 2. Проведи интервью итерациями по ≤3 вопроса. -3. Заведи `docs/.pm.json` с текущей версией канона. -4. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и +3. **Заведи OpenSpec: `openspec init --tools claude`.** Каталог `openspec/` — + часть канона, а не соседняя технология: в нём дом темы `requirements`, и без + него не работают ни `opsx:propose`, ни ревью дизайна, ни сверка требований. + Команда кладёт ещё `.claude/skills/openspec-*` и `.claude/commands/opsx/*` — + это её нормальная работа, не трогай их. +4. Заведи `docs/.pm.json` с текущей версией канона. +5. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и отдельным файлом не остаётся: два дома для одного замысла разойдутся на первом же уточнении. -5. Заведи скелет остальных по [скелетам](../canon/references/skeletons.md) — +6. Заведи скелет остальных по [скелетам](../canon/references/skeletons.md) — каждый с честной строкой. -6. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет +7. **Заполни `openspec/config.yaml`** по тем же скелетам. Файл из коробки — + закомментированный пример на английском; он **заменяется целиком**, потому что + нетронутый выглядит настроенным, а работает как пустой. Пиши туда только то, + что нужно **в момент порождения артефакта**: язык, правила именования + capability, придирки валидатора и **адреса** `docs/passport.md` и `CLAUDE.md`. + Инварианты, конвенции и правило ревью не пересказывай — у них есть дома, и + второй дом разойдётся с первым молча. +8. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет форматом целей и задач. -7. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о +9. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о незаполненных плейсхолдерах остаются: их закрывает не `init`, а работа. -8. Покажи человеку, что получилось, и **отдельным списком** — что выведено из - брифа, что предположено, что осталось неизвестным. Правят по этим строкам. +10. Покажи человеку, что получилось, и **отдельным списком** — что выведено из + брифа, что предположено, что осталось неизвестным. Правят по этим строкам. ## Что дальше