av-dev-pm расколот на av-dev-docs и av-dev-tasks
Плагин владел двумя разными вещами сразу — документацией проекта и учётом работ, — и это мешало обеим. Канон нельзя было поставить без задач, задачи без канона, а язык проектных текстов лежал внутри скилла canon и потому принадлежал половине. Теперь плагина два, каждый ставится сам по себе. av-dev-docs: скиллы canon, docs, init; агенты doc-consistency, doc-code-drift, doc-wording; скрипт docs.py. av-dev-tasks: скиллы tasks, session; агенты task-form, task-wording; скрипт tasks.py. Между собой они зовутся через пространство имён, а не по пути в чужое дерево. Все относительные ссылки, пересекшие границу плагина, сняты: tasks больше не указывает в canon, canon не указывает в tasks. Вместо ссылки — имя скилла и оговорка, что вызов может не разрешиться, и это исход, а не поломка. То, что нужно обоим дословно, стало вторым общим домом. Словарь «Сопровождение и эксплуатация» назван в трёх местах трёх плагинов — секция роадмапа, раздел «Эксплуатация» в architecture.md, тема ревью operations — и ни один из трёх им не владеет; он уехал в shared/operations.md, а canon.md и скилл задач везут копии. Три перечня «чем держат проект» уже разъезжались на «метриках и логах» против «мониторинга», так что ссылка тут не годится: плагин, поставленный в одиночку, получил бы указатель в никуда. Тем же способом язык: у av-dev-tasks появилась своя копия language.md. Копий стало 18 при 8 домах. Переименования разведены по смыслу, а не заменой строки: где речь о каноне — av-dev-docs, где об учёте задач — av-dev-tasks. В пайплайне таких мест одиннадцать, и оба адресата там встречаются вперемешку. Журналы (DECISIONS, TODO, HISTORY) намеренно не тронуты: они описывают состояние на момент записи. По той же причине оставлена наблюдённая строка в комментарии docs.py — она цитирует конфиг живого проекта, а не называет плагин. Не входит в этот заход и названо отдельно: слияние canon и docs в один скилл, разделение docs/.pm.json на два конфига и переезд openspec в пайплайн. Гейт зелёный: копии, фронтматтеры, диаграммы, json. Оба скрипта прогнаны после переезда — docs.py version и tasks.py check на фикстуре. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,593 @@
|
||||
# Канон документов проекта
|
||||
|
||||
**Версия 7.**
|
||||
|
||||
Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs`
|
||||
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
|
||||
работать будет то, которое прочитали последним. Меняется канон — меняется этот
|
||||
файл и появляется запись в [changelog.md](changelog.md).
|
||||
|
||||
## Зачем канон жёсткий
|
||||
|
||||
Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не
|
||||
техническая: проектов много, все малого и среднего размера, и ориентироваться в
|
||||
слегка похожих, но разных раскладках дороже, чем один раз привести их к общей.
|
||||
Рядом лежит OpenSpec, у которого структура тоже строгая.
|
||||
|
||||
Цена принята сознательно: плагин не переносится на чужой репозиторий как есть —
|
||||
чужой репозиторий **приводится** к канону скиллом `canon`.
|
||||
|
||||
Раскладка отвечает, **где** текст лежит и на какой вопрос отвечает. Каким он
|
||||
должен быть **словами** — общий для всех документов канона файл
|
||||
[language.md](language.md): информационный стиль, англицизмы, жаргон. Он
|
||||
относится и к задачам, и к решениям ADR, и к запискам разведки.
|
||||
|
||||
## Сопровождение и эксплуатация — целое и часть
|
||||
|
||||
**Копия.** Дом — `shared/operations.md` в репозитории плагинов: словарь
|
||||
делят роадмап, архитектура и тема ревью `operations`, то есть три плагина,
|
||||
и ни один из трёх им не владеет. Правится дом, а не этот файл.
|
||||
|
||||
<!-- копия: сопровождение-словарь из shared/operations.md -->
|
||||
|
||||
Одна тема живёт в трёх местах, и путать их слова нельзя.
|
||||
|
||||
**Сопровождение** — всё, чем держат проект: инструмент и сборка, процесс,
|
||||
выкладка, метрики и логи, инфраструктура, дежурство. **Эксплуатация** — его
|
||||
часть: работа системы на проде. Целое и часть, и никогда наоборот.
|
||||
|
||||
| Место | Уровень | Что там |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md`, секция `Сопровождение` | план | **работы**, которые собираемся делать: цели и их задачи |
|
||||
| `architecture.md`, раздел «Эксплуатация» | состояние | **как устроено сейчас**: где работает, что рядом, кто перезапускает |
|
||||
| тема ревью `operations` | оптика | **чем проверяем**: «это упало через неделю на проде» |
|
||||
|
||||
Слово **«поддержка» не употребляется вовсе** — в нём слышится помощь
|
||||
пользователю, а это другая работа.
|
||||
|
||||
**Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
|
||||
сообщает о своём состоянии» — возможность приложения, её место среди прочих
|
||||
целей: наблюдает пользователь сервиса. «Дежурный видит состояние на одном
|
||||
экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в разные
|
||||
секции роадмапа, и это верно — секции отвечают на разные вопросы.
|
||||
|
||||
<!-- /копия: сопровождение-словарь -->
|
||||
|
||||
## Раскладка
|
||||
|
||||
**Документ канона живёт файлом или каталогом.** `docs/security.md` и
|
||||
`docs/security/` — одно и то же; форму выбирает проект по объёму написанного, и
|
||||
переход между формами не меняет ни канон, ни версию. Обе формы сразу — ошибка:
|
||||
два дома для одного факта расходятся молча.
|
||||
|
||||
```
|
||||
CLAUDE.md памятка агенту: что это, стек, инварианты с
|
||||
severity, команды, семантика гейта, запреты
|
||||
AGENTS.md необязателен, лежит рядом; читается теми же
|
||||
docs/
|
||||
.pm.json версия канона и пути, нужные проверкам
|
||||
passport.md | passport/ зачем и для кого; чем НЕ является; сценарии
|
||||
architecture.md | architecture/ как сложено — обзор; окружение и эксплуатация
|
||||
database.md | database/ схема хранилища; представление данных и настройки
|
||||
security.md | security/ периметр; недоверенный вход; что вне модели
|
||||
conventions.md | conventions/ как пишем код; что механизировано
|
||||
research.md | research/ наблюдения и числа с провенансом
|
||||
adr.md | adr/ почему решено так; статусы, правило замены
|
||||
review.md | review/ настройка конвейера под проект + журнал дефектов
|
||||
<своя тема>.md | <своя тема>/ всё, что проект счёл нужным проверять
|
||||
tasks/ скилл tasks: items/, ROADMAP.md, BACKLOG.md,
|
||||
SPRINT.md, REJECTED.md
|
||||
openspec/
|
||||
config.yaml только нужды генерации артефактов + ссылки
|
||||
specs/<capability>/spec.md что система делает — нормативно
|
||||
changes/archive/ архив изменений с design.md — сырьё для ADR
|
||||
```
|
||||
|
||||
**У документа-каталога обязателен `README.md`** — вход, по которому его читают агенты.
|
||||
`adr/` в форме каталога держит ещё и `template.md`, а записи именуются
|
||||
`ADR-ГГГГ-ММ-ДД-slug.md`.
|
||||
|
||||
## Три категории документов
|
||||
|
||||
Раньше здесь стояло плоское правило «каждый документ `docs/` — тема ревью». Оно
|
||||
неверно ровно наполовину: паспорт и схема хранилища ревью нужны, но темами не
|
||||
являются, а журнал решений и журнал наблюдений ревью изменения не нужны вовсе.
|
||||
Плоское правило заставляло разметчика либо плодить фантомные темы, либо терять
|
||||
документы молча — а молчащая потеря и есть то, против чего канон написан.
|
||||
|
||||
**Разрез один и проверяемый: можно ли по документу сказать «в этом изменении
|
||||
сделано не так»?**
|
||||
|
||||
| Категория | Ответ на разрез | Что с ней делает ревью |
|
||||
| --- | --- | --- |
|
||||
| **тема** | да, прямо | заводит направление проверки и требует исполнителя |
|
||||
| **источник темы** | нет, но он задаёт границу, по которой судит чужая тема | читается как материал, своей темы не порождает |
|
||||
| **процессный документ** | нет: он про то, как мы работаем, а не про изменение | не судит по нему изменение |
|
||||
|
||||
| Документ | Категория | Куда питает |
|
||||
| --- | --- | --- |
|
||||
| `conventions.*` | тема | `conventions` |
|
||||
| `security.*` | тема | `security` |
|
||||
| `architecture.*` | тема | `architecture`; раздел эксплуатации — `operations` |
|
||||
| *свой документ проекта* | тема | своя тема, именем документа |
|
||||
| `passport.*` | источник | `architecture` — граница домена, «чем **не** является» |
|
||||
| `database.*` | источник | `operations` — схема и настройки с числами |
|
||||
| `CLAUDE.md`, `AGENTS.md` | источник | `autotests` (семантика гейта); инварианты — сквозные |
|
||||
| `openspec/specs/` | источник | `requirements` |
|
||||
| `openspec/config.yaml` | процессный | — (настройка порождения артефактов, слой **до** тем) |
|
||||
| `tasks/` | процессный | — |
|
||||
| `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) |
|
||||
| `adr.*` | процессный | — |
|
||||
| `research.*` | процессный | — |
|
||||
| `.pm.json` | процессный | — (служебный файл, не документ) |
|
||||
|
||||
**Список тем открытый, и это не послабление, а механизм.** Категории
|
||||
`источник` и `процессный` **закрыты** — они перечислены здесь поимённо и
|
||||
проектом не пополняются. Всё остальное, что проект кладёт в `docs/`, — тема: у
|
||||
конвейера есть приёмник для темы, к которой нет именной оптики, и заведён он
|
||||
ровно за этим. Завёл `docs/accessibility.md` — появилась тема `accessibility`, и
|
||||
она попадает в план каждого прогона.
|
||||
|
||||
Отсюда следствие, ради которого правило и заведено: **`docs/` — это конфигурация
|
||||
ревью.** Проект настраивает проверку тем, что пишет о себе, а не отдельным файлом
|
||||
настроек, который разошёлся бы с документами.
|
||||
|
||||
**«Не судит по нему» и «не открывает» — не одно и то же, и разница существенна.**
|
||||
`docs/review.*` проходы читают на каждом прогоне: там лежат вопросы по темам,
|
||||
журнал дефектов, типовые узлы и типовые ложноположительные. Это чтение конвейером
|
||||
**собственной настройки**, а не суждение об изменении, и потому оно законно.
|
||||
`adr/`, `research/` и `tasks/` не открывает никто: по ним изменение не судят, и
|
||||
настройкой конвейера они не являются.
|
||||
|
||||
**Процессный документ — не документ второго сорта.** `adr/` и `research/`
|
||||
проверяются наравне с остальными, но **сверкой документации**, а не прогоном
|
||||
ревью: ADR без ссылки на архивный `design.md`, замена без парного статуса, число
|
||||
без провенанса — это работа агентов `doc-consistency` и `doc-code-drift`, и она
|
||||
осталась там же, где была. Изменилось одно: прогон ревью не открывает их как
|
||||
критерий и не судит по ним изменение.
|
||||
|
||||
Цена этого решения записана, а не подразумевается: **расхождение изменения с
|
||||
записанным решением прогоном больше не ловится.** Раньше архитектурный проход
|
||||
читал `adr/` и мог сказать «здесь отменено решение ADR-2026-03-11, а парного
|
||||
статуса нет»; теперь это скажет только `doc-consistency` на сессии между
|
||||
спринтами. Сделка сознательная — ADR объясняет прошлое, а не предъявляет
|
||||
требование к изменению, и чтение всего каталога решений на каждой задаче
|
||||
оплачивалось на каждой, а срабатывало на единицах.
|
||||
|
||||
### Имена файлов английские, текст русский
|
||||
|
||||
**Текст документов русский; имена файлов, capability и задач — английские,
|
||||
kebab-case.** Причина не эстетическая: имя файла стоит в ссылках из других
|
||||
документов, в коммитах и в путях, которые набирают руками, — а кириллица в пути
|
||||
ломается по-разному в разных местах и не набирается на английской раскладке.
|
||||
|
||||
**Транслита не заводим.** Слаг именуется английским словом **по сути**, а не
|
||||
записью русского латиницей: `queue-as-table`, а не `ochered-tablicej`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается.
|
||||
|
||||
У ADR имя вдобавок несёт форму — `ADR-ГГГГ-ММ-ДД-slug.md`: по ней записи
|
||||
сортируются, и по ней же ищется дата решения.
|
||||
|
||||
`docs.py check` проверяет кириллицу и kebab-case **жёстко**, форму имени ADR —
|
||||
тоже, а транслит **эвристикой**, то есть замечанием: английское слово от
|
||||
транслита машина не отличает. Слаги каталога задач ведёт `tasks.py` — там та же
|
||||
проверка и тот же разрез.
|
||||
|
||||
**Переименование — не правка, а перенос ссылок**: делается одним проходом по
|
||||
всем местам, где имя упомянуто, иначе останутся битые ссылки. Для задач это
|
||||
умеет `tasks.py adopt`; для документов канона правит человек, а `docs.py` потом
|
||||
показывает, что ссылки целы.
|
||||
|
||||
## Роли документов и темы ревью
|
||||
|
||||
Одна строка на каждый — на какой вопрос он отвечает, в какой он категории и
|
||||
какую тему питает. **Кто именно закрывает тему, здесь не указано намеренно**: это
|
||||
зависит от метки прогона и меняется вместе с конвейером, а документ живёт
|
||||
дольше. Раскладку «тема → проход → глубина» держит скилл
|
||||
`av-dev-pipeline:review-pipeline`.
|
||||
|
||||
**Общего словаря у канона с конвейером ровно три вида имён: имена категорий,
|
||||
имена тем и имена меток.** Категорий три — `тема`, `источник`, `процессный`;
|
||||
**меток тоже три, и они закрыты: `small`, `medium`, `large`.** Метка это итог
|
||||
классификации задачи, и по ней конвейер выбирает исполнителей на обеих стадиях
|
||||
ревью; проект её не выдумывает, а только уточняет триггеры. Ими проект и
|
||||
настраивает ревью — вопросами по темам и триггерами метки. **Имён проходов канон не называет нигде**, включая вывод `docs.py`:
|
||||
проход переименовывается и переезжает между метками, и канон, назвавший его, в
|
||||
этот день соврёт молча. Обратное направление законно — конвейер называет
|
||||
документы канона поимённо, потому что он их читатель.
|
||||
|
||||
| Документ | Вопрос | Категория и тема |
|
||||
| --- | --- | --- |
|
||||
| `CLAUDE.md`, `AGENTS.md` | что нельзя нарушать, чем краснеет гейт | источник: `autotests`; инварианты — сквозные, во все темы |
|
||||
| `passport.*` | зачем и для кого, чем это **не** является | источник: `architecture` |
|
||||
| `architecture.*` | как сложено и где что работает | тема `architecture`; раздел эксплуатации — `operations` |
|
||||
| `database.*` | что лежит в хранилище и какими настройками | источник: `operations` |
|
||||
| `security.*` | против кого защищаемся и что вне модели | тема `security` |
|
||||
| `conventions.*` | как мы пишем код | тема `conventions` |
|
||||
| `openspec/specs/` | что система делает — нормативно | источник: `requirements` |
|
||||
| `research.*` | что показала реальность, а не документация | процессный |
|
||||
| `adr.*` | почему решено именно так | процессный |
|
||||
| `review.*` | как настроен конвейер и что уже проскакивало | процессный: слой **над** темами |
|
||||
| `tasks/` | что делаем и в каком порядке | процессный |
|
||||
| *свой документ проекта* | что проект счёл нужным проверять | **своя тема**, именем документа |
|
||||
|
||||
### `passport.md`
|
||||
|
||||
Цель; закрытый список потребителей и что каждому нужно; **чем целью не
|
||||
является** — это граница домена, по которой архитектурный проход судит о
|
||||
переносе понятия; типовые сценарии; мера, по которой проект считается удавшимся;
|
||||
референсы, у кого подсматривать.
|
||||
|
||||
### `architecture.md` — **обзор, не поведение**
|
||||
|
||||
Принципы; компоненты **со ссылками на capability**, а не с пересказом их
|
||||
требований; **единые точки проекта** — где генерируются идентификаторы и время,
|
||||
где единственный парсер входного формата, где маппинг доменной ошибки в код
|
||||
ответа, где общий путь приёма (это материал для вопроса «не появился ли второй
|
||||
способ»); внешние границы и форматы чужих систем; окружение — где работает, что
|
||||
рядом, кто перезапускает; **внешние зависимости поимённо** и чем каждая
|
||||
отказывает (не только «падает», но и «отвечает медленно», «молчит», «отдаёт
|
||||
мусор»); кто заметит отказ и когда; характер потока — непрерывный, по запросу,
|
||||
по расписанию; деплой; открытые вопросы.
|
||||
|
||||
**Обратимости здесь нет** — её единственный дом `CLAUDE.md`: туда ходят пять
|
||||
проходов, и раздвоение адреса означало бы, что проект написал ответ, а ревью его
|
||||
не прочитало.
|
||||
|
||||
**Поведение системы сюда не пишется.** Его нормативный дом — `openspec/specs/`,
|
||||
куда `opsx:archive` вливает дельты; второй дом синхронизировать руками
|
||||
невозможно, и он разойдётся.
|
||||
|
||||
Раздел, ещё не разнесённый при переезде, помечается маркером долга:
|
||||
|
||||
```
|
||||
<!-- канон: поведение → openspec/specs/<capability> -->
|
||||
```
|
||||
|
||||
`docs.py` считает маркеры и печатает остаток числом. Гейт от них **не краснеет**:
|
||||
это долг, а не отказ, иначе постепенный переезд стал бы невозможен.
|
||||
|
||||
### `database.md`
|
||||
|
||||
Схема: таблицы, ключи, связи, правило времени и идентификаторов. Плюс то, чего
|
||||
нет в схеме, но без чего замер не превращается в находку: **чем физически лежит
|
||||
запись** (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи
|
||||
(распаковка целиком, read-modify-write), и **настройки с числовым значением** —
|
||||
таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен.
|
||||
|
||||
Конвенции идентификаторов и именования — не схема, они в `conventions/`.
|
||||
|
||||
### `security.md`
|
||||
|
||||
**Периметр первой строкой.** «Сервис открыт наружу» и «контур доверенный,
|
||||
публичного интернета здесь нет, не выдумывай его» — противоположные постановки
|
||||
под одним заголовком, и разбор темы `security` между ними сам не выберет. Контур ещё
|
||||
не развёрнут — назови **оба** периметра, целевой и сегодняшний, и скажи прямо,
|
||||
против какого строятся находки.
|
||||
|
||||
Дальше: что недоверенное и каким каналом приходит; **из чего строятся пути и
|
||||
ключи** (раскладка файлов, состав координатного ключа, имя каталога) — отсюда
|
||||
строится выход за пределы песочницы; что разграничивает доступ; что
|
||||
чувствительнее чего; **что вне модели** — перечислить явно.
|
||||
|
||||
### `conventions/`
|
||||
|
||||
Прозой остаётся **только то, что не выражается правилом**. `README.md` держит
|
||||
индекс, правило промоута и **перечень уже механизированного** со ссылкой на
|
||||
место механизации — конфиг линтера, собственный анализатор, тест-сканер
|
||||
исходников. Не названное место механизации означает, что проход добросовестно
|
||||
проверит уже проверенное.
|
||||
|
||||
### `research/`
|
||||
|
||||
Наблюдения за внешним миром: что реально шлёт источник, чем документация формата
|
||||
расходится с практикой, какие числа сняты с живого потока. **Числа — с
|
||||
провенансом**, то есть с командой или условиями, которыми получены.
|
||||
`README.md` — как снималось и индекс тем.
|
||||
|
||||
Число без источника проход обязан читать как условие, а не как замер. Число, чей
|
||||
источник по ссылке не подтвердился, не выбрасывается и не переписывается по
|
||||
догадке — остаётся с пометкой «расходится с источником: там <что нашли>».
|
||||
|
||||
### `adr/`
|
||||
|
||||
**ADR — промоут поверх архивных `design.md`, а не второе сочинение.** Запись
|
||||
цитирует решение и ссылается на `openspec/changes/archive/<id>/design.md`.
|
||||
|
||||
Заводится, когда верно одно из трёх:
|
||||
|
||||
<!-- дом: adr-когда-заводить -->
|
||||
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||||
- **намеренный отказ** от очевидного подхода;
|
||||
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
|
||||
«заменено на».
|
||||
<!-- /дом: adr-когда-заводить -->
|
||||
|
||||
Не заводится для рутины и для того, что видно из кода и `git log`.
|
||||
|
||||
Записи неизменяемы: передумали — заводится новая, старая получает статус.
|
||||
Активная запись статуса не имеет.
|
||||
|
||||
**Статус живёт полем меты записи**, там же, где дата и источник:
|
||||
`- **Статус:** заменено на ADR-…` либо `- **Статус:** устарело`. Места ему в
|
||||
шаблоне не отводилось, и каждая запись изобретала своё — то абзацем, то
|
||||
заголовком; в таблице `adr/README.md` статус при этом обязан быть, а брать его
|
||||
оттуда, где он у каждого свой, нельзя.
|
||||
|
||||
### `review.md`
|
||||
|
||||
Два раздела с разными сроками жизни.
|
||||
|
||||
**Настройка конвейера под проект**, пять подразделов с точными именами — по ним
|
||||
проходы находят свой кусок:
|
||||
|
||||
- **Типовые узлы** — рода узлов проекта и 3–5 проверяемых свойств к каждому;
|
||||
- **Типовые ложноположительные** — находки, которые здесь выглядят убедительно и
|
||||
всегда неверны, каждая со строкой «почему здесь это не дефект»;
|
||||
- **Вопросы по темам** — в форме `<тема>: <вопрос> (<провенанс>)`. **Не по именам
|
||||
проходов**: проход уезжает между метками, а тема остаётся, и вопрос,
|
||||
адресованный проходу, перестал бы задаваться молча в тот день, когда тот уехал
|
||||
в старшую метку. Задаёт вопрос тот, кто закрывает тему на этом прогоне.
|
||||
Адресовать можно только теме: `passport`, `database`, `adr`, `research` и
|
||||
`review` — не темы, и вопрос, адресованный им, не задаст никто;
|
||||
- **Триггеры метки** — проектная конкретизация правила выбора метки ревью,
|
||||
**тремя списками**. Два поднимают, по одному на ось: что в этом проекте считается
|
||||
**крупным** (объём: сколько узлов и слоёв трогает) и что считается
|
||||
**незнакомым** (форма решения: известна до начала или нащупывается по ходу).
|
||||
Любая из двух осей поднимает прогон до `large`, старшей метки, — а она
|
||||
рассчитана на 5–10% задач. Третий список — что считается **мелким** (опускает
|
||||
до `small`); он один, потому что вниз метку опускает только совпадение обеих
|
||||
осей сразу. Перечнем мест, узлами или capability, а не вторым определением
|
||||
класса. Уточняет умолчания, а не отменяет их. Рабочее умолчание — `medium`:
|
||||
миграция схемы и публичный контракт метку **не** поднимают, их проверяют
|
||||
проходы, которые в `medium` и так есть;
|
||||
- **Недоступно проверке** — два подраздела, оба **по темам**: «не проверит ни
|
||||
один проход» (принципиальная граница, по факту промаха не пересматривается) и
|
||||
«перестали проверять сознательно» (пересматривается первым). Тема, у которой в
|
||||
проекте нет дома, сюда не пишется: её и так называет план каждого прогона.
|
||||
|
||||
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
|
||||
**проскочил / пойман ревью**. Проскочившие — проверочный набор для калибровки конвейера,
|
||||
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
|
||||
воспроизводимые, однажды оказавшиеся правдой.
|
||||
|
||||
### `tasks/`
|
||||
|
||||
Раскладку, форму записи и команды держит скилл `tasks` — канон фиксирует имена
|
||||
файлов (`items/`, `ROADMAP.md`, `BACKLOG.md`, `SPRINT.md`, `REJECTED.md`) и то,
|
||||
от чего зависит, читается ли проект как продукт.
|
||||
|
||||
**`ROADMAP.md` отвечает на «что приложение уже умеет и чего ещё не умеет».** Это
|
||||
не очередь работ: цель — **возможность приложения**, задача — шаг к ней.
|
||||
Достигнутая цель из роадмапа **не исчезает** — строка с датой переезжает в
|
||||
секцию достигнутого, потому что «что умеет» и есть половина вопроса, ради
|
||||
которого документ открывают. Вторым домом поведения роадмап при этом не
|
||||
становится: нормативное поведение живёт в `openspec/specs/`, роадмап отвечает,
|
||||
**когда и в каком порядке** оно появилось.
|
||||
|
||||
**У каждой записи есть тип, и тип решает, что с ней можно делать.** Дом типа —
|
||||
поле меты `Тип` первой строкой; эмодзи в заголовке от него производна. Словарь
|
||||
закрыт:
|
||||
|
||||
| Тип | Что это |
|
||||
| --- | --- |
|
||||
| 🎯 `goal` | возможность приложения |
|
||||
| ✨ `feature` | снаружи появляется то, чего не было |
|
||||
| 🐞 `fix` | поведение расходится с заявленным |
|
||||
| 🧹 `chore` | обслуживание, поведение не меняется |
|
||||
| 🔬 `research` | исход — знание, а не изменение |
|
||||
|
||||
**Схемы записи здесь нет намеренно.** Какие разделы тип требует, нужна ли ему
|
||||
цель и берётся ли он в спринт — скилл `av-dev-tasks:tasks`, раздел «Тип
|
||||
записи», подробно — по файлу на тип в его `references/task-<тип>.md`. Ссылки в
|
||||
дерево того плагина здесь нет намеренно: он ставится отдельно, и путь наружу
|
||||
разрешился бы не всегда. Канон фиксирует **словарь**, потому что
|
||||
от него зависит, читается ли проект как продукт; схема — механика ведения задач,
|
||||
и второй её экземпляр разошёлся бы с первым (он и разошёлся: канон успел
|
||||
объявить цель у `fix` запрещённой, хотя она там необязательна).
|
||||
|
||||
Схема требуется **к взятию в спринт**, а не к заведению: беклог пополняется чаще,
|
||||
чем разбирается, и требование на входе выгоняло бы в заметки то, что должно
|
||||
лежать задачей. Запись, не собравшая разделы своего типа, — законное состояние
|
||||
беклога; невзятой её делает `sprint take`.
|
||||
|
||||
Отдельного типа для незаполненной записи нет: «ещё не описано» — состояние, а не
|
||||
род работы, и называется оно **`research` без раздела «Вопрос»**. Такая запись в
|
||||
спринт не берётся и лежит в конце своей категории.
|
||||
|
||||
Раскладку, форму записи и алгоритм работы над каждым типом держит скилл `tasks`.
|
||||
|
||||
### `CLAUDE.md`
|
||||
|
||||
Что это и стек; **инварианты с severity рядом с формулировкой** — по ним проходы
|
||||
присваивают `critical`, поэтому severity стоит здесь, а не выводится каждым
|
||||
проходом заново; команды; **семантика гейта** — чем краснеет безусловно и почему,
|
||||
где логи, что означает исход, чего в гейте намеренно нет, **кто и когда обязан
|
||||
гонять дорогое вне гейта**.
|
||||
|
||||
Плюс то, что нужно git-операциям и проходам и не выводится ниоткуда:
|
||||
|
||||
- **имя основной ветки** — от неё считается база диффа
|
||||
(`git merge-base HEAD <ветка>`), в неё вливает батч, от неё ветвятся задачи.
|
||||
Угадывание между `master` и `main` ломает интеграцию целиком;
|
||||
- **что запускать запрещено, с путями** — рабочая БД, боевой каталог данных,
|
||||
внешние сервисы. Запретом с путями, а не «будь осторожен»;
|
||||
- **где `testdata`** и что в них лежит; **куда писать временное**;
|
||||
- **что считается необратимым** — единственный дом: от обратимости зависит вся
|
||||
шкала ранжирования триажа и право проходов на `critical`;
|
||||
- **общий станок**, врывающийся в замороженный спринт; **ориентир по размеру
|
||||
спринта**.
|
||||
|
||||
### `openspec/config.yaml`
|
||||
|
||||
**Только нужды генерации артефактов** — язык, правила именования capability,
|
||||
придирки валидатора 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/`; без этих двух адресов его пишут, не зная
|
||||
ни границы домена, ни инвариантов.
|
||||
5. **Ключи под `rules:` — имена артефактов схемы** (`proposal`, `specs`,
|
||||
`design`, `tasks`). Правило, адресованное несуществующему артефакту, не
|
||||
применяется и об этом молчит: `rules.spec` вместо `rules.specs` — конфиг,
|
||||
выглядящий написанным и не работающий.
|
||||
|
||||
**Схема и перечень артефактов — слепок чужого инструмента, и он стареет.**
|
||||
OpenSpec переименует артефакт или сменит схему — правила под прежним именем
|
||||
перестанут действовать молча, а канон будет продолжать требовать прежнее.
|
||||
Поэтому за свежестью слепка следит машина: `check` сравнивает `major.minor`
|
||||
установленного OpenSpec с версией, на которой форма сверялась, и при расхождении
|
||||
даёт **замечание** (не отказ: патч-версии формы не меняют, а нагоняй на каждый
|
||||
багфикс приучает пролистывать блок). Перепроверяет `docs.py openspec-form` — он
|
||||
спрашивает сам инструмент и печатает, что разошлось. **Чинится это в плагине, а
|
||||
не в проекте:** константы скрипта, скелет и запись в журнал версий канона.
|
||||
|
||||
Шестого — «нет ли здесь пересказа» — машина не проверяет: отличить ссылку от
|
||||
пересказа она не умеет. Это работа `doc-consistency`, и раздел «Что проверяет
|
||||
машина, а что человек» называет её строкой.
|
||||
|
||||
Форма — [skeletons.md](skeletons.md).
|
||||
|
||||
## Правило единственного дома
|
||||
|
||||
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
|
||||
|
||||
<!-- дом: карта-домов -->
|
||||
| Факт | Дом |
|
||||
| --- | --- |
|
||||
| поведение системы | `openspec/specs/<capability>/spec.md` |
|
||||
| почему решено так | `adr/`, источник — архивный `design.md` |
|
||||
| граница домена, «чем не является» | `passport.md` |
|
||||
| инвариант и его severity | `CLAUDE.md` |
|
||||
| что приложение умеет и чего не умеет; порядок работ | `docs/tasks/ROADMAP.md` |
|
||||
| измеренное число | `research/` |
|
||||
| настройка с числовым значением | `database.md` |
|
||||
| периметр и модель угроз | `security.md` |
|
||||
| что необратимо | `CLAUDE.md` — **не** `architecture.md` |
|
||||
| единые точки проекта | `architecture.md` |
|
||||
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
|
||||
| что уже механизировано правилом | `conventions/README.md` |
|
||||
<!-- /дом: карта-домов -->
|
||||
|
||||
## Пустое называется пустым
|
||||
|
||||
Скелет канона заводится **целиком** с первого дня. Незаполненный документ держит
|
||||
**одну честную информативную строку**, а не заглушку:
|
||||
|
||||
- «внешних зависимостей нет — смотри на диск и на СУБД»;
|
||||
- «наблюдений на живых данных нет: внешний источник один, формат документирован»;
|
||||
- «прецедентов не накоплено»;
|
||||
- «сознательно ничего не отключали»;
|
||||
- «архитектуры пока нет: кода нет, заводится первой задачей».
|
||||
|
||||
Проход читает такую строку **как факт** и не тратит на неё обязательный вопрос.
|
||||
Отсутствие файла он не может прочитать никак, а «TBD» читает как пробел —
|
||||
поэтому `docs.py check` отличает честную строку от нетронутого плейсхолдера
|
||||
шаблона и напоминает о втором.
|
||||
|
||||
## Слотов нет
|
||||
|
||||
Файлы и каталоги, которых в каноне **нет**, и куда уезжает их содержимое:
|
||||
|
||||
| Было | Куда |
|
||||
| --- | --- |
|
||||
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
|
||||
| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) |
|
||||
| `docs/drafts/` | идея → запись `research`; отказ → ADR; порядок → `ROADMAP.md`; размышление → `opsx:explore` |
|
||||
| `docs/plan.md` | `docs/tasks/ROADMAP.md` |
|
||||
| `BRIEF.md` | `passport.md` |
|
||||
| `docs/backlog/` | `docs/tasks/` |
|
||||
| `docs/review-journal.md`, `docs/review/journal.md` | `docs/review.md` |
|
||||
|
||||
## Что проверяет машина, а что человек
|
||||
|
||||
Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон
|
||||
соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего.
|
||||
|
||||
| Проверяет `docs.py` | Судит агент | Какой |
|
||||
| --- | --- | --- |
|
||||
| отсутствующие пути канона | смысловой дубль документа и capability | `doc-consistency` |
|
||||
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` | `doc-consistency` |
|
||||
| имя файла не kebab-case латиницей; форма имени ADR | транслит в имени — сверх эвристики | `doc-wording` |
|
||||
| битые относительные ссылки | прямое противоречие между документами | `doc-consistency` |
|
||||
| версия канона и её отставание | достаточность честной строки в пустом слоте | `doc-consistency` |
|
||||
| нетронутый плейсхолдер шаблона | ADR без ссылки на `design.md`, замена без парного статуса | `doc-consistency` |
|
||||
| маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` |
|
||||
| миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` |
|
||||
| capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` |
|
||||
| `openspec/config.yaml`: имя, `schema`, незаменённый пример, адреса паспорта и `CLAUDE.md`, ключи `rules` против артефактов схемы | **пересказ документа канона в `context` вместо ссылки** | `doc-consistency` |
|
||||
| версия OpenSpec разошлась с той, на которой сверена форма `config.yaml` | придирки валидатора: сменились ли они | никакой — проявляются отказом `openspec validate --strict` |
|
||||
| | связность и читаемость | `doc-wording` |
|
||||
|
||||
**Агентов двое, и разведены они по глубине, а не по охвату.** `doc-consistency`
|
||||
читает только `docs/` и `openspec/`, `doc-code-drift` — весь репозиторий и гоняет
|
||||
читающие команды. Слитый агент делал бы одну половину поверхностной; тот же
|
||||
разрез, что между `task-form` и `task-wording`.
|
||||
|
||||
**Зовутся оба одинаково — раз в спринт на сессии, а также после `adopt` и после
|
||||
`upgrade`, на весь канон разом.** Не на синке документации: `doc-consistency` на
|
||||
`opus` по каждой сделанной задаче не окупается, а расхождение между двумя документами по
|
||||
определению требует двух, и на большинстве задач синк правит один. Пачка,
|
||||
отбираемая работой, вдобавок не видит того, чего работа не касалась, — а именно
|
||||
там расхождение и живёт: правка отменяет решение в одном документе, парный статус
|
||||
нужен в другом.
|
||||
|
||||
**Перечень фактов, которые `doc-code-drift` сверяет с кодом, закрыт** — имя
|
||||
основной ветки, команды, пути, зависимости поимённо, настройки с числовым
|
||||
значением, единые точки проекта, capability, проверяемые инварианты. «Сверить
|
||||
архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт
|
||||
правдоподобную труху вместо находок.
|
||||
|
||||
## `docs/.pm.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"canon": 7,
|
||||
"migrations": "internal/store/migrations",
|
||||
"tasks": {
|
||||
"backlog": "INDEX.md"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`canon` — версия канона, под которую проект приведён, целым числом: обратной
|
||||
совместимости у канона нет, есть «приведён» и «не приведён». `migrations` — путь
|
||||
каталога миграций, если БД есть; по нему `docs.py` делает сверку с
|
||||
`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего
|
||||
`<tasks>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
|
||||
Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`,
|
||||
`roadmap`, `sprint`, `rejected`, `sprint_section`, `oracle_word` и заголовки
|
||||
разделов тела: `criteria_heading`, `surface_heading`, `questions_heading`,
|
||||
`completion_heading`, `repro_heading`, `question_heading`, `answer_heading`,
|
||||
`scope_heading`), и ключ пишется, лишь когда имя отличается от умолчания.
|
||||
**Словаря типов здесь нет** — он закрыт каноном, а не настраивается проектом:
|
||||
настраиваемый словарь типов разъехался бы на синонимах ровно так же, как
|
||||
открытый. **Категорий беклога здесь тоже нет:** их дом — заголовки `##` самого
|
||||
индекса, и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py`
|
||||
отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с
|
||||
задачами целиком.
|
||||
|
||||
Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py`
|
||||
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
|
||||
строкой, а не молчит.
|
||||
Reference in New Issue
Block a user