init заводит openspec сам; конфиг стал слотом канона
Каталог openspec/ был предпосылкой, о которой канон говорил, но за которой не следил. openspec/specs/ объявлен домом темы requirements, config.yaml описан абзацем — а заводилось всё руками, и не проверялось ничего. Новый проект выходил из init с полным каноном документов и без каталога, без которого не работают ни opsx:propose, ни ревью дизайна, ни сверка требований. Теперь init делает openspec init --tools claude шагом 3, до первого документа, а adopt заводит его тем же способом, если на переводимом проекте его нет. Команда названа поимённо в трёх местах — скилле, каноне и отказе docs.py: отказ без команды заставляет искать её в другом месте. Файл из коробки оказался хуже отсутствующего, и потому проверяется машиной. openspec init кладёт config.yaml, где context и rules — закомментированный пример на английском. Такой файл читается как настроенный: он есть, он валиден, имя правильное. Работает он как пустой, и узнаётся это по уже написанному предложению — на другом языке, с capability по имени пакета, без единого SHALL. docs.py проверяет четыре вещи, каждая про молчащий пробел: каталог есть; имя именно config.yaml (config.yml OpenSpec не читает и об этом не сообщает); context и rules.specs не остались примером, а правила называют SHALL; context называет passport и CLAUDE.md. Последние два обязательны по порядку работы: предложение пишется до того, как кто-либо откроет docs/, и без этих строк его пишут, не зная ни границы домена, ни инвариантов. Форма конфига записана скелетом и сформулирована разрезом: утверждение, которое можно опровергнуть, открыв другой файл проекта, — пересказ; строка, которая говорит, какой файл открыть, — ссылка. Машина этот разрез не проверяет, отличить одно от другого она не умеет; он отдан doc-consistency отдельным абзацем правила «один факт — один дом», и config.yaml добавлен ему во вход. Место второго дома там самое частое: context читается при порождении каждого артефакта, туда удобно дописать «чтобы агент знал», и так заводятся копии инвариантов, конвенций, состава гейта и правил ревью. Образец лёг в канон, а не в конвейер, как планировало решение C: форма документа принадлежит владельцу канона документов, конвейер её читатель. Иначе av-dev-pipeline завёл бы описание файла, который заводит и проверяет av-dev-pm. Канон повышен до версии 7 с записью, выполнимой upgrade: завести openspec, привести config.yaml к скелету, вычистить из context пересказ, проверить имя файла, поднять номер в .pm.json. Проверка прогнана на четырёх фикстурах — свежий openspec init, два живых проекта и пустой каталог; отличает все четыре случая. Решение — 47. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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. **Заполненная форма и заполненный смысл — разные вещи, и первая маскирует
|
||||
вторую.** Файл на месте, валиден, с правильным именем — и пуст по существу:
|
||||
это худший вид пробела, потому что выглядит он как его отсутствие.
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@ description: "Конвейер ревью изменения, устроенны
|
||||
упадут на «нет такого скилла», а `review-specs` останется без источника
|
||||
требований. **Проект без OpenSpec этим конвейером не проверяется** — подключай
|
||||
OpenSpec, а не понижай прогон: ветка деградации здесь не пишется, потому что
|
||||
непроверенная ветка деградации хуже честного отказа.
|
||||
непроверенная ветка деградации хуже честного отказа. Заводить руками не надо:
|
||||
`av-dev-pm:init` делает `openspec init` на новом проекте, `canon adopt` — на
|
||||
переводимом, и оба кладут `openspec/config.yaml` канонической формы.
|
||||
- **Документы канона** — см. следующий раздел.
|
||||
- **Проектные копии этих скиллов и агентов удаляются при установке.** Если в
|
||||
проекте уже лежат свои `.claude/skills/review-pipeline`,
|
||||
|
||||
@@ -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. **Прямое противоречие между документами.** Самое дорогое, что ты находишь, и
|
||||
искать его надо адресно, а не вычитыванием подряд. Пары, которые расходятся
|
||||
чаще прочих:
|
||||
|
||||
@@ -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` тоже
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/` — тема ревью**. Правило оказалось
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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. Покажи человеку, что получилось, и **отдельным списком** — что выведено из
|
||||
брифа, что предположено, что осталось неизвестным. Правят по этим строкам.
|
||||
|
||||
## Что дальше
|
||||
|
||||
|
||||
Reference in New Issue
Block a user