av-dev-pm: плагин переименован, заведены канон документов и скиллы init/canon/docs
- av-dev-tasks → av-dev-pm; канон определён единственным reference-файлом, который читают все три новых скилла - canon: check/adopt/upgrade плюс docs.py — раскладка, битые ссылки, версия, маркеры долга, сверки миграций и capability с документацией - tasks и session: путь docs/tasks жёсткий, конфиг переехал в docs/.pm.json, слот «Команда учёта задач» убран в пользу вызова скилла, раздел «Стимулы» переписан под совпавших приёмщика и исполнителя
This commit is contained in:
@@ -6,24 +6,24 @@
|
|||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "av-dev-backlog",
|
"name": "av-dev-pm",
|
||||||
"source": "./av-dev-backlog",
|
"source": "./av-dev-pm",
|
||||||
"description": "Ведение беклога задач как каталога markdown-файлов: заведение из диалога, разбор находок ревью, груминг, приоритизация, декомпозиция, штурм идей."
|
"description": "Управление продуктом: канон документов проекта, задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт проекта интервью по брифу и приведение существующего к канону."
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "av-dev-tasks",
|
|
||||||
"source": "./av-dev-tasks",
|
|
||||||
"description": "Управление задачами: цели вместо приоритетов, спринт под одну цель с заморозкой набора, вопросы и блокеры, каденция «разбор — переоценка — набор». Преемник av-dev-backlog."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "av-dev-pipeline",
|
"name": "av-dev-pipeline",
|
||||||
"source": "./av-dev-pipeline",
|
"source": "./av-dev-pipeline",
|
||||||
"description": "Проведение задачи через цикл SDD и конвейер ревью с обязательным триажем, плюс прогон нескольких задач разом. Проектная специфика — из файла-брифа."
|
"description": "Проведение задачи через цикл SDD и конвейер ревью с обязательным триажем, плюс прогон нескольких задач разом. Требует OpenSpec; проектная специфика — из документов канона."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "av-dev-git",
|
"name": "av-dev-git",
|
||||||
"source": "./av-dev-git",
|
"source": "./av-dev-git",
|
||||||
"description": "Git-обвязка для личных проектов. Скилл commit — сообщения коммитов в личном стиле (русский, scope-префикс, средняя детализация, без co-authored)."
|
"description": "Git-обвязка для личных проектов. Скилл commit — сообщения коммитов в личном стиле (русский, scope-префикс, средняя детализация, без co-authored)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "av-dev-backlog",
|
||||||
|
"source": "./av-dev-backlog",
|
||||||
|
"description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями. Оставлен до перевода последнего проекта; новые проекты не подключают."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
__pycache__/
|
|
||||||
*.pyc
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "av-dev-backlog",
|
"name": "av-dev-backlog",
|
||||||
"description": "Ведение беклога задач как каталога markdown-файлов (одна задача = один файл + строка в индексе README). Заведение, груминг, приоритизация, декомпозиция, штурм идей, разбор находок ревью.",
|
"description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями, без целей и спринтов. Оставлен до перевода последнего проекта, который на нём ещё живёт; новые проекты не подключают.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Anton Vakhrushev",
|
"name": "Anton Vakhrushev",
|
||||||
"email": "anwinged@gmail.com"
|
"email": "anwinged@gmail.com"
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
---
|
---
|
||||||
name: backlog
|
name: backlog
|
||||||
description: Работа с беклогом задач как с каталогом markdown-файлов (одна задача = один файл + строка в индексе README). Заведение задачи из диалога, разбор находок аудита/ревью в задачи, груминг (интерактивная чистка неактуального), приоритизация, декомпозиция на независимо полезные части, мозговой штурм идеи. Использовать, когда просят добавить задачу/идею в беклог, превратить находки ревью в задачи, разобрать беклог, расставить приоритеты, разбить задачу или проработать идею. Не реализует задачи — этим занимается пайплайн задачи проекта.
|
description: УСТАРЕЛ — используй скилл av-dev-pm:tasks. Старый формат беклога (один каталог задач, индекс README, приоритеты секциями, без целей и спринтов). Вызывать ТОЛЬКО в проекте, который на этот формат ещё не переведён, и только если прямо названо имя backlog. Во всех остальных случаях, включая любую просьбу завести задачу, идею или разобрать находки ревью, работает av-dev-pm:tasks.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **Этот скилл устарел.** Формат заменён каноном `docs/tasks/` из плагина
|
||||||
|
> `av-dev-pm` (цели вместо приоритетов, спринт с заморозкой набора, `REJECTED.md`
|
||||||
|
> с причинами). Перевод проекта делает скилл `av-dev-pm:canon`. Скилл оставлен до
|
||||||
|
> перевода последнего проекта, который на нём ещё живёт, и будет удалён.
|
||||||
|
|
||||||
|
|
||||||
# Беклог
|
# Беклог
|
||||||
|
|
||||||
Беклог — каталог markdown-файлов: одна задача = один файл `<slug>.md`, плюс
|
Беклог — каталог markdown-файлов: одна задача = один файл `<slug>.md`, плюс
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "av-dev-pm",
|
||||||
|
"description": "Управление продуктом: канон документов проекта (паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), задачи и цели вместо приоритетов, спринт под одну цель с заморозкой набора, старт нового проекта интервью по брифу и приведение существующего к канону. Не выполняет задачи — этим занимается пайплайн проекта.",
|
||||||
|
"author": {
|
||||||
|
"name": "Anton Vakhrushev",
|
||||||
|
"email": "anwinged@gmail.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
---
|
||||||
|
name: canon
|
||||||
|
description: Привести проект к канону документов av-dev и держать его в соответствии — три операции одной машиной сравнения. check — что разошлось с текущей версией канона; adopt — перевод проекта из любой прежней раскладки (docs/specs, drafts, backlog, BRIEF.md, review-brief) в канон с переносом файлов; upgrade — повышение проекта с версии канона N до текущей по журналу версий. Использовать, когда просят проверить документацию проекта, перевести проект на канон, обновить его под новую версию канона или когда пришли в старый проект и надо понять, что в нём не так. Заведение нового проекта с нуля — скилл init.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Приведение проекта к канону
|
||||||
|
|
||||||
|
Три операции, одна машина сравнения с разными исходами:
|
||||||
|
|
||||||
|
| Операция | Когда | Исход |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `check` | начало сессии, шаг синка, гейт | что разошлось |
|
||||||
|
| `adopt` | проект в чужой раскладке | перенос в канон |
|
||||||
|
| `upgrade` | канон вырос, проект отстал | по журналу версий |
|
||||||
|
|
||||||
|
**Определение канона — [references/canon.md](references/canon.md).** Здесь оно не
|
||||||
|
пересказывается: два описания одной раскладки разъедутся, и работать будет то,
|
||||||
|
которое прочитали последним. Прочитай его **до** первой правки.
|
||||||
|
|
||||||
|
Журнал версий — [references/changelog.md](references/changelog.md).
|
||||||
|
|
||||||
|
## Три правила, из которых всё следует
|
||||||
|
|
||||||
|
1. **Сперва карта, потом файлы.** Человеку показывается, что найдено, как
|
||||||
|
разложилось и **что не разложилось**, — и только после подтверждения
|
||||||
|
переносится хоть один файл. Массовый перенос без подтверждения разгребать
|
||||||
|
дороже, чем согласовать.
|
||||||
|
2. **Ничего не терять.** Содержимое переезжает целиком; ссылки чинятся тем же
|
||||||
|
проходом, что и перенос. Старый файл удаляется **только** после того, как
|
||||||
|
всё его содержимое нашло дом, и это названо поимённо.
|
||||||
|
3. **Что не классифицировалось — назвать.** Проглоченный абзац выглядит как
|
||||||
|
«всё перенеслось». Список «не разложилось» идёт в доклад целиком, с причиной
|
||||||
|
по каждому пункту.
|
||||||
|
|
||||||
|
## Инструмент
|
||||||
|
|
||||||
|
```
|
||||||
|
ds="$CLAUDE_PLUGIN_ROOT/skills/canon/scripts/docs.py"
|
||||||
|
|
||||||
|
python3 $ds check --dir <корень> [--base <rev>] # раскладка, ссылки, версия, сверки
|
||||||
|
python3 $ds version --dir <корень> # версия канона скрипта и проекта
|
||||||
|
```
|
||||||
|
|
||||||
|
**Коды выхода — тот же словарь, что у `tasks.py`:** 0 сошлось, 1 дрейф, 2 ошибка
|
||||||
|
употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте.
|
||||||
|
|
||||||
|
Различать 1 и 3 обязательно: «дрейф раскладки» — рабочая ситуация, «это не
|
||||||
|
корень проекта» — нерабочая.
|
||||||
|
|
||||||
|
### Граница механизируемого — объявляется вслух
|
||||||
|
|
||||||
|
Скрипт печатает её сам последним абзацем, и **эту строку из доклада выбрасывать
|
||||||
|
нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов
|
||||||
|
три лишние, хуже отсутствующего.
|
||||||
|
|
||||||
|
Машина проверяет пути, лишние файлы, битые ссылки, версию, нетронутые
|
||||||
|
плейсхолдеры, маркеры долга и две сверки с кодом. **Ты** судишь о том, чего она
|
||||||
|
не умеет:
|
||||||
|
|
||||||
|
- **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что
|
||||||
|
capability `recognition`. Файлы разные, содержание одно;
|
||||||
|
- **поведение, оставшееся в `architecture.md`** — раздел на 900 строк с
|
||||||
|
требованиями вместо обзора;
|
||||||
|
- **достаточность честной строки** — «внешних зависимостей нет» это факт,
|
||||||
|
«TBD» — пробел;
|
||||||
|
- **протухший факт** — документ ссылается на то, чего в коде уже нет.
|
||||||
|
|
||||||
|
## `check`
|
||||||
|
|
||||||
|
1. `docs.py check`, при наличии базы диффа — с `--base`.
|
||||||
|
2. Прочитай то, что скрипт проверить не может (список выше), по документам,
|
||||||
|
которых касалась работа. Не «заодно по всему `docs/`».
|
||||||
|
3. Доклад: вывод скрипта строкой исхода, твои находки поимённо, **граница
|
||||||
|
покрытия** — что смотрел и чего не смотрел.
|
||||||
|
|
||||||
|
Дрейф раскладки чинится переносом; смысловые находки — это либо правка
|
||||||
|
документа, либо задача, если работы больше чем на абзац.
|
||||||
|
|
||||||
|
## `adopt` — проект в чужой раскладке
|
||||||
|
|
||||||
|
### 1. Осмотрись
|
||||||
|
|
||||||
|
`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый
|
||||||
|
уезжает. Плюс прочитай: `CLAUDE.md`, корневые `*.md`, `openspec/specs/` (список
|
||||||
|
capability), `openspec/config.yaml`.
|
||||||
|
|
||||||
|
### 2. Составь карту
|
||||||
|
|
||||||
|
Каждый найденный файл получает строку: **куда едет, целиком или разбирается, что
|
||||||
|
делать с оригиналом**. Разбор `docs/specs/` — самое дорогое место, и он делается
|
||||||
|
поимённо по capability:
|
||||||
|
|
||||||
|
| Что в файле | Куда |
|
||||||
|
| --- | --- |
|
||||||
|
| требования, сценарии, поведение | `openspec/specs/<capability>/spec.md` — **или уже там**, тогда файл дубль |
|
||||||
|
| компоненты, транспорты, раскладка, деплой | `docs/architecture.md` |
|
||||||
|
| конвенции чужой системы, формат чужих данных | `docs/research/` |
|
||||||
|
| обоснование принятого решения | `docs/adr/` |
|
||||||
|
|
||||||
|
**Дубль удаляется только после поимённой сверки**: открыть спеку capability,
|
||||||
|
открыть файл, убедиться, что в файле нет ничего сверх спеки. Нашлось сверх —
|
||||||
|
сперва переезжает в спеку дельтой, потом файл удаляется.
|
||||||
|
|
||||||
|
### 3. Покажи карту человеку
|
||||||
|
|
||||||
|
`AskUserQuestion`, **не больше трёх вопросов за итерацию**, рекомендация первым
|
||||||
|
вариантом. Показывается: сколько файлов, куда каждый, спорные отнесения, список
|
||||||
|
«не разложилось». Механику (порядок строк, имена файлов внутри `research/`) не
|
||||||
|
выноси — это не развилка.
|
||||||
|
|
||||||
|
### 4. Перенеси
|
||||||
|
|
||||||
|
Порядок важен — он минимизирует окно, в котором ссылки битые:
|
||||||
|
|
||||||
|
1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть;
|
||||||
|
2. каталоги канона и скелет: незаполненное — **одной честной информативной
|
||||||
|
строкой**, а не «TBD» (см. canon.md, «Пустое называется пустым»);
|
||||||
|
3. переносы содержимого;
|
||||||
|
4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он
|
||||||
|
владеет форматом задач, включая переименование транслитных слагов в
|
||||||
|
английские вместе с починкой перекрёстных ссылок;
|
||||||
|
5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
|
||||||
|
`CLAUDE.md`, `README.md`;
|
||||||
|
6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
|
||||||
|
7. шаг `docs.py check` в гейт проекта;
|
||||||
|
8. `docs.py check` — до зелёного в механизируемой части.
|
||||||
|
|
||||||
|
### 5. Объяви переходное состояние
|
||||||
|
|
||||||
|
Сразу после переноса канон **заполнен не весь**, и это нормально, но обязано
|
||||||
|
быть названо, иначе следующий агент примет скелет за поломку.
|
||||||
|
|
||||||
|
Печатается по факту: сколько документов стоят честной строкой вместо
|
||||||
|
содержания, сколько маркеров долга в `architecture.md`, сколько задач без
|
||||||
|
критериев приёмки. Закрывается порциями по ходу работы, а не одним заходом.
|
||||||
|
|
||||||
|
## `upgrade` — канон вырос
|
||||||
|
|
||||||
|
1. `docs.py version` — версия проекта и версия скрипта.
|
||||||
|
2. Проект новее скрипта — **обнови маркетплейс**, а не проект: это отстал
|
||||||
|
плагин.
|
||||||
|
3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии
|
||||||
|
проекта до текущей и делай названное в каждой записи. Записи независимы и
|
||||||
|
применяются по порядку.
|
||||||
|
4. Подними `canon` в `docs/.pm.json` до текущей.
|
||||||
|
5. `docs.py check`.
|
||||||
|
|
||||||
|
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
|
||||||
|
это дефект журнала, и о нём надо сказать, а не догадываться.
|
||||||
|
|
||||||
|
## Чего этот скилл не делает
|
||||||
|
|
||||||
|
- **Не сочиняет содержание.** Пустой слот получает честную строку о том, что его
|
||||||
|
наполнить пока нечем, а не выдуманный абзац. Придуманный периметр модели угроз
|
||||||
|
хуже отсутствующего: по нему будут строиться находки.
|
||||||
|
- **Не удаляет то, чьё содержимое не нашло дом.** Оригинал живёт, пока не
|
||||||
|
названо поимённо, куда переехал каждый его кусок.
|
||||||
|
- **Не ведёт содержимое канона** — это скилл `docs`. Здесь только раскладка.
|
||||||
|
- **Не заводит проект с нуля** — это скилл `init`.
|
||||||
|
- **Не правит историю.** В старых коммитах старые пути остаются, и это нормально.
|
||||||
|
|
||||||
|
## Доклад
|
||||||
|
|
||||||
|
- Что нашёл `docs.py`: код выхода и число пунктов дрейфа.
|
||||||
|
- Что перенесено: файл → дом, числом и поимённо для спорного.
|
||||||
|
- **Удалённые дубли** — с указанием, против какой спеки сверялся каждый.
|
||||||
|
- **Не разложилось** — поимённо, с причиной.
|
||||||
|
- Переходное состояние числами: честных строк, маркеров долга, задач без
|
||||||
|
критериев.
|
||||||
|
- **Граница покрытия**: что проверила машина, что судил ты, чего не смотрел
|
||||||
|
никто.
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
# Канон документов проекта
|
||||||
|
|
||||||
|
**Версия 1.**
|
||||||
|
|
||||||
|
Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs`
|
||||||
|
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
|
||||||
|
работать будет то, которое прочитали последним. Меняется канон — меняется этот
|
||||||
|
файл и появляется запись в [changelog.md](changelog.md).
|
||||||
|
|
||||||
|
## Зачем канон жёсткий
|
||||||
|
|
||||||
|
Пути фиксированы, и проект под них подгоняется, а не наоборот. Причина не
|
||||||
|
техническая: проектов много, все малого и среднего размера, и ориентироваться в
|
||||||
|
слегка похожих, но разных раскладках дороже, чем один раз привести их к общей.
|
||||||
|
Рядом лежит OpenSpec, у которого структура тоже строгая.
|
||||||
|
|
||||||
|
Цена принята сознательно: плагин не переносится на чужой репозиторий как есть —
|
||||||
|
чужой репозиторий **приводится** к канону скиллом `canon`.
|
||||||
|
|
||||||
|
## Раскладка
|
||||||
|
|
||||||
|
```
|
||||||
|
CLAUDE.md памятка агенту: что это, стек, инварианты с
|
||||||
|
severity, команды, семантика гейта, запреты
|
||||||
|
docs/
|
||||||
|
.pm.json версия канона и пути, нужные проверкам
|
||||||
|
passport.md зачем и для кого; чем НЕ является; сценарии
|
||||||
|
architecture.md как сложено — обзор; окружение и эксплуатация
|
||||||
|
database.md схема хранилища; представление данных и настройки
|
||||||
|
security.md периметр; недоверенный вход; что вне модели
|
||||||
|
conventions/
|
||||||
|
README.md индекс, правило промоута, что механизировано
|
||||||
|
<тема>.md
|
||||||
|
research/
|
||||||
|
README.md как снималось, индекс
|
||||||
|
<тема>.md наблюдения и числа с провенансом
|
||||||
|
adr/
|
||||||
|
README.md индекс записей, статусы, правило замены
|
||||||
|
template.md
|
||||||
|
ADR-ГГГГ-ММ-ДД-slug.md
|
||||||
|
review.md настройка конвейера под проект + журнал дефектов
|
||||||
|
tasks/ скилл tasks: items/, PLAN.md, BACKLOG.md,
|
||||||
|
SPRINT.md, REJECTED.md
|
||||||
|
openspec/
|
||||||
|
config.yaml только нужды генерации артефактов + ссылки
|
||||||
|
specs/<capability>/spec.md что система делает — нормативно
|
||||||
|
changes/archive/ архив изменений с design.md — сырьё для ADR
|
||||||
|
```
|
||||||
|
|
||||||
|
Текст документов — русский; слаги файлов, capability и задач — английские,
|
||||||
|
kebab-case.
|
||||||
|
|
||||||
|
## Роли документов
|
||||||
|
|
||||||
|
Одна строка на каждый — на какой вопрос он отвечает и кто его читает.
|
||||||
|
|
||||||
|
| Документ | Вопрос | Кто читает, кроме человека |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `CLAUDE.md` | что нельзя нарушать, чем краснеет гейт | все агенты, всегда |
|
||||||
|
| `passport.md` | зачем и для кого, чем это **не** является | `architecture`, `rubric`, `reimpl`, `specs` |
|
||||||
|
| `architecture.md` | как сложено и где что работает | все проходы ревью |
|
||||||
|
| `database.md` | что лежит в хранилище и какими настройками | `ops`, `adversary`, `reimpl` |
|
||||||
|
| `security.md` | против кого защищаемся и что вне модели | `adversary` |
|
||||||
|
| `conventions/` | как мы пишем код | `code` |
|
||||||
|
| `research/` | что показала реальность, а не документация | `specs`, `reimpl`, `ops` |
|
||||||
|
| `adr/` | почему решено именно так | `architecture` |
|
||||||
|
| `review.md` | как настроен конвейер и что уже проскакивало | `triage`, каждый проход — свою часть |
|
||||||
|
| `openspec/specs/` | что система делает — нормативно | `specs` |
|
||||||
|
|
||||||
|
### `passport.md`
|
||||||
|
|
||||||
|
Цель; закрытый список потребителей и что каждому нужно; **чем целью не
|
||||||
|
является** — это граница домена, по которой архитектурный проход судит о
|
||||||
|
переносе понятия; типовые сценарии; мера, по которой проект считается удавшимся;
|
||||||
|
референсы, у кого подсматривать.
|
||||||
|
|
||||||
|
### `architecture.md` — **обзор, не поведение**
|
||||||
|
|
||||||
|
Принципы; компоненты **со ссылками на capability**, а не с пересказом их
|
||||||
|
требований; внешние границы и форматы чужих систем; окружение — где работает,
|
||||||
|
что рядом, кто перезапускает; **внешние зависимости поимённо** и чем каждая
|
||||||
|
отказывает (не только «падает», но и «отвечает медленно», «молчит», «отдаёт
|
||||||
|
мусор»); кто заметит отказ и когда; характер потока — непрерывный, по запросу,
|
||||||
|
по расписанию; что обратимо, а что нет; деплой; открытые вопросы.
|
||||||
|
|
||||||
|
**Поведение системы сюда не пишется.** Его нормативный дом — `openspec/specs/`,
|
||||||
|
куда `opsx:archive` вливает дельты; второй дом синхронизировать руками
|
||||||
|
невозможно, и он разойдётся.
|
||||||
|
|
||||||
|
Раздел, ещё не разнесённый при переезде, помечается маркером долга:
|
||||||
|
|
||||||
|
```
|
||||||
|
<!-- канон: поведение → openspec/specs/<capability> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
`docs.py` считает маркеры и печатает остаток числом. Гейт от них **не краснеет**:
|
||||||
|
это долг, а не отказ, иначе постепенный переезд стал бы невозможен.
|
||||||
|
|
||||||
|
### `database.md`
|
||||||
|
|
||||||
|
Схема: таблицы, ключи, связи, правило времени и идентификаторов. Плюс то, чего
|
||||||
|
нет в схеме, но без чего замер не превращается в находку: **чем физически лежит
|
||||||
|
запись** (сжатый BLOB, JSON-строка, колонки), что происходит при чтении и записи
|
||||||
|
(распаковка целиком, read-modify-write), и **настройки с числовым значением** —
|
||||||
|
таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен.
|
||||||
|
|
||||||
|
Конвенции идентификаторов и именования — не схема, они в `conventions/`.
|
||||||
|
|
||||||
|
### `security.md`
|
||||||
|
|
||||||
|
**Периметр первой строкой.** «Сервис открыт наружу» и «контур доверенный,
|
||||||
|
публичного интернета здесь нет, не выдумывай его» — противоположные постановки
|
||||||
|
под одним заголовком, и враждебный проход между ними сам не выберет. Контур ещё
|
||||||
|
не развёрнут — назови **оба** периметра, целевой и сегодняшний, и скажи прямо,
|
||||||
|
против какого строятся находки.
|
||||||
|
|
||||||
|
Дальше: что недоверенное и каким каналом приходит; **из чего строятся пути и
|
||||||
|
ключи** (раскладка файлов, состав координатного ключа, имя каталога) — отсюда
|
||||||
|
строится выход за пределы песочницы; что разграничивает доступ; что
|
||||||
|
чувствительнее чего; **что вне модели** — перечислить явно.
|
||||||
|
|
||||||
|
### `conventions/`
|
||||||
|
|
||||||
|
Прозой остаётся **только то, что не выражается правилом**. `README.md` держит
|
||||||
|
индекс, правило промоута и **перечень уже механизированного** со ссылкой на
|
||||||
|
место механизации — конфиг линтера, собственный анализатор, тест-сканер
|
||||||
|
исходников. Непойманное место механизации означает, что проход добросовестно
|
||||||
|
проверит уже проверенное.
|
||||||
|
|
||||||
|
### `research/`
|
||||||
|
|
||||||
|
Наблюдения за внешним миром: что реально шлёт источник, чем документация формата
|
||||||
|
расходится с практикой, какие числа сняты с живого потока. **Числа — с
|
||||||
|
провенансом**, то есть с командой или условиями, которыми получены.
|
||||||
|
`README.md` — как снималось и индекс тем.
|
||||||
|
|
||||||
|
Число без источника проход обязан читать как условие, а не как замер. Число, чей
|
||||||
|
источник по ссылке не подтвердился, не выбрасывается и не переписывается по
|
||||||
|
догадке — остаётся с пометкой «расходится с источником: там <что нашли>».
|
||||||
|
|
||||||
|
### `adr/`
|
||||||
|
|
||||||
|
**ADR — промоут поверх архивных `design.md`, а не второе сочинение.** Запись
|
||||||
|
цитирует решение и ссылается на `openspec/changes/archive/<id>/design.md`.
|
||||||
|
|
||||||
|
Заводится, когда верно одно из трёх:
|
||||||
|
|
||||||
|
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||||||
|
- **намеренный отказ** от очевидного подхода;
|
||||||
|
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
|
||||||
|
«заменено на».
|
||||||
|
|
||||||
|
Не заводится для рутины и для того, что видно из кода и `git log`.
|
||||||
|
|
||||||
|
Записи неизменяемы: передумали — заводится новая, старая получает статус.
|
||||||
|
Активная запись статуса не имеет.
|
||||||
|
|
||||||
|
### `review.md`
|
||||||
|
|
||||||
|
Два раздела с разными сроками жизни.
|
||||||
|
|
||||||
|
**Настройка конвейера под проект:** типовые узлы (рода узлов и 3–5 проверяемых
|
||||||
|
свойств к каждому); типовые ложноположительные — находки, которые здесь выглядят
|
||||||
|
убедительно и всегда неверны; вопросы к проходам поимённо с провенансом;
|
||||||
|
недоступно проверке — два подраздела, «не проверит ни один проход»
|
||||||
|
(принципиальная граница, по факту промаха не пересматривается) и «перестали
|
||||||
|
проверять сознательно» (пересматривается первым).
|
||||||
|
|
||||||
|
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
|
||||||
|
**проскочил / пойман ревью**. Проскочившие — эвал-сет для калибровки конвейера,
|
||||||
|
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
|
||||||
|
воспроизводимые, однажды оказавшиеся правдой.
|
||||||
|
|
||||||
|
### `CLAUDE.md`
|
||||||
|
|
||||||
|
Что это и стек; **инварианты с severity рядом с формулировкой** — по ним
|
||||||
|
проходы присваивают `critical`, поэтому severity стоит здесь, а не выводится
|
||||||
|
каждым проходом заново; команды; **семантика гейта** — чем краснеет безусловно и
|
||||||
|
почему, где логи, что означает исход, чего в гейте намеренно нет, **кто и когда
|
||||||
|
обязан гонять дорогое вне гейта**; что запускать запрещено, с путями; что
|
||||||
|
считается необратимым; общий станок, врывающийся в замороженный спринт; ориентир
|
||||||
|
по размеру спринта.
|
||||||
|
|
||||||
|
### `openspec/config.yaml`
|
||||||
|
|
||||||
|
**Только нужды генерации артефактов** — язык, правила именования capability,
|
||||||
|
придирки валидатора RFC 2119 — плюс ссылки на документы канона. Правило ревью,
|
||||||
|
пересказ конвенций и инварианты сюда не пишутся: у них есть свои дома, и второй
|
||||||
|
дом разойдётся на первой же правке.
|
||||||
|
|
||||||
|
## Правило единственного дома
|
||||||
|
|
||||||
|
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
|
||||||
|
|
||||||
|
| Факт | Дом |
|
||||||
|
| --- | --- |
|
||||||
|
| поведение системы | `openspec/specs/<capability>/spec.md` |
|
||||||
|
| почему решено так | `adr/`, источник — архивный `design.md` |
|
||||||
|
| граница домена, «чем не является» | `passport.md` |
|
||||||
|
| инвариант и его severity | `CLAUDE.md` |
|
||||||
|
| порядок работ и его обоснование | `docs/tasks/PLAN.md` |
|
||||||
|
| измеренное число | `research/` |
|
||||||
|
| настройка с числовым значением | `database.md` |
|
||||||
|
| периметр и модель угроз | `security.md` |
|
||||||
|
| что уже механизировано правилом | `conventions/README.md` |
|
||||||
|
|
||||||
|
## Пустое называется пустым
|
||||||
|
|
||||||
|
Скелет канона заводится **целиком** с первого дня. Незаполненный документ держит
|
||||||
|
**одну честную информативную строку**, а не заглушку:
|
||||||
|
|
||||||
|
- «внешних зависимостей нет — смотри на диск и на СУБД»;
|
||||||
|
- «наблюдений на живых данных нет: внешний источник один, формат документирован»;
|
||||||
|
- «прецедентов не накоплено»;
|
||||||
|
- «сознательно ничего не отключали»;
|
||||||
|
- «архитектуры пока нет: кода нет, заводится первой задачей».
|
||||||
|
|
||||||
|
Проход читает такую строку **как факт** и не тратит на неё обязательный вопрос.
|
||||||
|
Отсутствие файла он не может прочитать никак, а «TBD» читает как пробел —
|
||||||
|
поэтому `docs.py check` отличает честную строку от нетронутого плейсхолдера
|
||||||
|
шаблона и напоминает о втором.
|
||||||
|
|
||||||
|
## Слотов нет
|
||||||
|
|
||||||
|
Файлы и каталоги, которых в каноне **нет**, и куда уезжает их содержимое:
|
||||||
|
|
||||||
|
| Было | Куда |
|
||||||
|
| --- | --- |
|
||||||
|
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
|
||||||
|
| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) |
|
||||||
|
| `docs/drafts/` | идея → задача `[idea]`; отказ → ADR; порядок → `PLAN.md`; размышление → `opsx:explore` |
|
||||||
|
| `docs/plan.md` | `docs/tasks/PLAN.md` |
|
||||||
|
| `BRIEF.md` | `passport.md` |
|
||||||
|
| `docs/backlog/` | `docs/tasks/` |
|
||||||
|
| `docs/review-journal.md`, `docs/review/journal.md` | `docs/review.md` |
|
||||||
|
|
||||||
|
## Что проверяет машина, а что человек
|
||||||
|
|
||||||
|
Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон
|
||||||
|
соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего.
|
||||||
|
|
||||||
|
| Проверяет `docs.py` | Судит агент |
|
||||||
|
| --- | --- |
|
||||||
|
| отсутствующие пути канона | смысловой дубль документа и capability |
|
||||||
|
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` |
|
||||||
|
| битые относительные ссылки | протухший факт, разошедшийся с кодом |
|
||||||
|
| версия канона и её отставание | достаточность честной строки в пустом слоте |
|
||||||
|
| нетронутый плейсхолдер шаблона | связность и читаемость |
|
||||||
|
| маркеры долга — числом | |
|
||||||
|
| миграция изменена, а `database.md` нет | |
|
||||||
|
| capability без упоминания в `architecture.md` | |
|
||||||
|
|
||||||
|
## `docs/.pm.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"canon": 1,
|
||||||
|
"migrations": "internal/store/migrations",
|
||||||
|
"tasks": {
|
||||||
|
"sections": ["ядро", "инфра"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`canon` — версия канона, под которую проект приведён, целым числом: обратной
|
||||||
|
совместимости у канона нет, есть «приведён» и «не приведён». `migrations` — путь
|
||||||
|
каталога миграций, если БД есть; по нему `docs.py` делает сверку с
|
||||||
|
`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего
|
||||||
|
`<tasks>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
|
||||||
|
|
||||||
|
Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py`
|
||||||
|
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
|
||||||
|
строкой, а не молчит.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Журнал версий канона
|
||||||
|
|
||||||
|
Одна запись на версию. Проект знает свою версию из `docs/.pm.json`; `canon
|
||||||
|
upgrade` идёт по записям снизу вверх от версии проекта до текущей и делает то,
|
||||||
|
что в них названо.
|
||||||
|
|
||||||
|
Правило записи: **что добавилось, что переехало, что удалено, что сделать
|
||||||
|
проекту**. Без последнего пункта запись бесполезна — по ней и работает
|
||||||
|
`upgrade`.
|
||||||
|
|
||||||
|
Версия — целое число. Обратной совместимости у канона нет: есть «приведён» и «не
|
||||||
|
приведён».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Версия 1 — 2026-08-03
|
||||||
|
|
||||||
|
Первая версия. Проект любой прежней раскладки приводится к ней скиллом `canon`
|
||||||
|
в режиме `adopt`, а не `upgrade`.
|
||||||
|
|
||||||
|
**Что вводится:** раскладка целиком — см. [canon.md](canon.md).
|
||||||
|
|
||||||
|
**Что сделать проекту, который приходит из свободной раскладки:**
|
||||||
|
|
||||||
|
1. `docs/.pm.json` с `{"canon": 1}` и путём миграций, если БД есть.
|
||||||
|
2. Скелет канона целиком; незаполненное — одной честной строкой.
|
||||||
|
3. `docs/specs/` разобрать: поведение — в `openspec/specs/`, обзор — в
|
||||||
|
`docs/architecture.md`, знание о чужих системах — в `docs/research/`.
|
||||||
|
Дубли capability удалить, сверив поимённо.
|
||||||
|
4. `docs/plan.md` → `docs/tasks/PLAN.md` линией целей.
|
||||||
|
5. `BRIEF.md` → `docs/passport.md`.
|
||||||
|
6. `docs/backlog/` → `docs/tasks/`.
|
||||||
|
7. `docs/review-journal.md` или `docs/review/journal.md` → `docs/review.md`,
|
||||||
|
плюс раздел настройки конвейера.
|
||||||
|
8. `docs/drafts/` растворить: идея → задача `[idea]`, намеренный отказ → ADR,
|
||||||
|
порядок работ → `PLAN.md`.
|
||||||
|
9. `docs/review-brief.md`, если заводился, удалить: его разделы разошлись по
|
||||||
|
документам канона.
|
||||||
|
10. `conventions.md` → `conventions/`, `local-research.md` → `research/`.
|
||||||
|
11. Завести `docs/security.md` с периметром первой строкой и `docs/adr/`.
|
||||||
|
12. В `CLAUDE.md`: severity рядом с каждым инвариантом, семантика гейта,
|
||||||
|
запреты с путями; убрать раздел «Процесс», если он пересказывает пайплайн.
|
||||||
|
13. В `openspec/config.yaml` оставить только нужды генерации и ссылки.
|
||||||
|
14. Добавить шаг `docs.py check` в гейт проекта.
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
# Скелеты документов канона
|
||||||
|
|
||||||
|
Что кладут `init` и `canon adopt` в незаполненный слот. Правило одно:
|
||||||
|
**честная информативная строка вместо заглушки**. Проход читает строку как факт;
|
||||||
|
`<!-- заполнить: … -->` он читает как пробел, и `docs.py check` о таком
|
||||||
|
плейсхолдере напоминает.
|
||||||
|
|
||||||
|
Плейсхолдер ставится только там, где ответ **обязан** быть и его не спросили.
|
||||||
|
Всё, чего в проекте пока просто нет, описывается словами, а не плейсхолдером.
|
||||||
|
|
||||||
|
## `docs/passport.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Паспорт проекта
|
||||||
|
|
||||||
|
Зачем это и для кого. [architecture.md](architecture.md) отвечает «как
|
||||||
|
устроено», [tasks/PLAN.md](tasks/PLAN.md) — «в каком порядке», паспорт —
|
||||||
|
«зачем и для кого».
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
<!-- заполнить: одна фраза без технических деталей -->
|
||||||
|
|
||||||
|
**Потребители** — список закрытый: он определяет, что считать нужным, а что
|
||||||
|
интересным.
|
||||||
|
|
||||||
|
| Кто | Что ему нужно от нас |
|
||||||
|
| --- | --- |
|
||||||
|
|
||||||
|
Цель достигнута, когда:
|
||||||
|
|
||||||
|
## Что целью не является
|
||||||
|
|
||||||
|
Граница домена. По ней архитектурный проход судит, не перенесено ли понятие
|
||||||
|
через границу.
|
||||||
|
|
||||||
|
## Типовые сценарии
|
||||||
|
|
||||||
|
## Референсы
|
||||||
|
|
||||||
|
Где смотреть prior art, когда упёрлись.
|
||||||
|
```
|
||||||
|
|
||||||
|
## `docs/architecture.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Архитектура
|
||||||
|
|
||||||
|
Обзор: как сложено и где что работает. **Поведение системы здесь не
|
||||||
|
описывается** — его нормативный дом `openspec/specs/`.
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
|
||||||
|
## Компоненты
|
||||||
|
|
||||||
|
Каждый — строкой со ссылкой на capability, а не пересказом её требований.
|
||||||
|
|
||||||
|
## Внешние границы и форматы
|
||||||
|
|
||||||
|
## Эксплуатация
|
||||||
|
|
||||||
|
- Где работает, что рядом, кто перезапускает:
|
||||||
|
- Внешние зависимости поимённо и чем каждая отказывает (падает, отвечает
|
||||||
|
медленно, молчит, отдаёт мусор):
|
||||||
|
- Кто заметит отказ и когда:
|
||||||
|
- Характер потока (непрерывный, по запросу, по расписанию):
|
||||||
|
- Что обратимо, а что нет:
|
||||||
|
|
||||||
|
## Деплой
|
||||||
|
|
||||||
|
## Открытые вопросы
|
||||||
|
```
|
||||||
|
|
||||||
|
Пустой проект: «Архитектуры пока нет: кода нет. Наполняется первой задачей.»
|
||||||
|
Нет внешних зависимостей: «Внешних зависимостей нет — смотри на диск и на СУБД.»
|
||||||
|
|
||||||
|
## `docs/database.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Схема хранилища
|
||||||
|
|
||||||
|
СУБД, миграции, правило времени и идентификаторов.
|
||||||
|
|
||||||
|
## Таблицы
|
||||||
|
|
||||||
|
## Представление данных
|
||||||
|
|
||||||
|
Чем физически лежит запись и что происходит при чтении и записи.
|
||||||
|
|
||||||
|
## Настройки с числовым значением
|
||||||
|
|
||||||
|
Таймаут занятости, режим журналирования, лимит тела, размер пула, ретеншен.
|
||||||
|
Без них замер не превращается в находку: пик памяти — аномалия только рядом
|
||||||
|
со строкой «запись лежит сжатой и распаковывается целиком».
|
||||||
|
```
|
||||||
|
|
||||||
|
Нет БД — файла нет, и в `docs/.pm.json` нет ключа `migrations`.
|
||||||
|
|
||||||
|
## `docs/security.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Модель угроз
|
||||||
|
|
||||||
|
## Периметр
|
||||||
|
|
||||||
|
<!-- заполнить: первой строкой, против кого защищаемся -->
|
||||||
|
|
||||||
|
Контур не развёрнут — назови оба периметра, целевой и сегодняшний, и скажи
|
||||||
|
прямо, против какого строятся находки.
|
||||||
|
|
||||||
|
## Недоверенный вход
|
||||||
|
|
||||||
|
Что приходит извне и каким каналом: тело запроса, файл, аргумент команды,
|
||||||
|
ответ внешней системы, содержимое архива.
|
||||||
|
|
||||||
|
## Из чего строятся пути и ключи
|
||||||
|
|
||||||
|
Раскладка файлов на диске, состав координатного ключа записи, имя каталога.
|
||||||
|
Отсюда строится выход за пределы песочницы.
|
||||||
|
|
||||||
|
## Что разграничивает доступ
|
||||||
|
|
||||||
|
## Что чувствительнее чего
|
||||||
|
|
||||||
|
## Что вне модели
|
||||||
|
|
||||||
|
Перечислить явно. Пустой пункт означает, что враждебный проход выдумает угрозу
|
||||||
|
сам, и находка никогда не будет исправлена.
|
||||||
|
```
|
||||||
|
|
||||||
|
## `docs/conventions/README.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Конвенции кода
|
||||||
|
|
||||||
|
Как мы пишем код — в отличие от `openspec/specs/`, который описывает, что
|
||||||
|
система делает.
|
||||||
|
|
||||||
|
**Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее
|
||||||
|
правилом линтера, отсюда удаляется и переезжает в перечень ниже.
|
||||||
|
|
||||||
|
## Записи
|
||||||
|
|
||||||
|
## Механизировано
|
||||||
|
|
||||||
|
| Правило | Где механизировано |
|
||||||
|
| --- | --- |
|
||||||
|
|
||||||
|
Непойманное место механизации означает, что проход по конвенциям будет
|
||||||
|
добросовестно проверять уже проверенное.
|
||||||
|
```
|
||||||
|
|
||||||
|
Пустой проект: «Конвенций пока нет: код не написан. Наполняется по мере
|
||||||
|
реального трения, а не вперёд.»
|
||||||
|
|
||||||
|
## `docs/research/README.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Разведка
|
||||||
|
|
||||||
|
Наблюдения за внешним миром: что реально шлёт источник, чем документация
|
||||||
|
формата расходится с практикой. Источник истины — этот каталог, а не чужая
|
||||||
|
документация.
|
||||||
|
|
||||||
|
**Каждый вывод — с числами и командой, которой получен**, чтобы его можно было
|
||||||
|
перепроверить.
|
||||||
|
|
||||||
|
## Как снималось
|
||||||
|
|
||||||
|
## Записи
|
||||||
|
```
|
||||||
|
|
||||||
|
Нет внешних источников: «Внешних источников данных нет — разведка неприменима.»
|
||||||
|
|
||||||
|
## `docs/adr/README.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Журнал решений
|
||||||
|
|
||||||
|
Одна запись — одно решение. **ADR это промоут поверх архивного `design.md`**,
|
||||||
|
а не второе сочинение: запись цитирует решение и ссылается на
|
||||||
|
`openspec/changes/archive/<id>/design.md`.
|
||||||
|
|
||||||
|
## Когда заводить
|
||||||
|
|
||||||
|
Верно одно из трёх:
|
||||||
|
|
||||||
|
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||||||
|
- **намеренный отказ** от очевидного подхода;
|
||||||
|
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус.
|
||||||
|
|
||||||
|
Не заводить для рутины и того, что видно из кода и `git log`.
|
||||||
|
|
||||||
|
## Соглашения
|
||||||
|
|
||||||
|
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, слаг английский, дата — когда решение
|
||||||
|
реально принято.
|
||||||
|
- Записи неизменяемы: передумали — новая запись, старой ставится статус.
|
||||||
|
- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и
|
||||||
|
`устарело`.
|
||||||
|
|
||||||
|
## Записи
|
||||||
|
|
||||||
|
Новые сверху.
|
||||||
|
|
||||||
|
| Дата | Запись | Статус |
|
||||||
|
| --- | --- | --- |
|
||||||
|
```
|
||||||
|
|
||||||
|
## `docs/adr/template.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Краткий заголовок решения
|
||||||
|
|
||||||
|
- Дата: ГГГГ-ММ-ДД
|
||||||
|
- Источник: openspec/changes/archive/<id>/design.md
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
Что именно решено — одной фразой.
|
||||||
|
|
||||||
|
## Почему
|
||||||
|
|
||||||
|
Намерение и причина. Цитата из источника, а не пересказ. Пиши так, чтобы через
|
||||||
|
год было понятно без чтения переписки.
|
||||||
|
|
||||||
|
## Последствия
|
||||||
|
|
||||||
|
- `+` что стало лучше.
|
||||||
|
- `−` чем платим: ограничения, риски, нагрузка на поддержку.
|
||||||
|
```
|
||||||
|
|
||||||
|
## `docs/review.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Ревью: настройка и журнал
|
||||||
|
|
||||||
|
## Как настроен конвейер
|
||||||
|
|
||||||
|
### Типовые узлы
|
||||||
|
|
||||||
|
Рода узлов проекта и 3–5 проверяемых свойств к каждому. Рода, а не инвентарь
|
||||||
|
пакетов: род, который проект задумал, но ещё не написал, включать полезно.
|
||||||
|
|
||||||
|
### Типовые ложноположительные
|
||||||
|
|
||||||
|
Находки, которые здесь выглядят убедительно и всегда неверны. Каждая — с одной
|
||||||
|
строкой «почему здесь это не дефект».
|
||||||
|
|
||||||
|
### Вопросы к проходам
|
||||||
|
|
||||||
|
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Главный источник — журнал ниже.
|
||||||
|
|
||||||
|
### Недоступно проверке
|
||||||
|
|
||||||
|
**Не проверит ни один проход** — принципиальная граница; по факту промаха не
|
||||||
|
пересматривается.
|
||||||
|
|
||||||
|
**Перестали проверять сознательно** — что, когда и почему, со ссылкой на запись
|
||||||
|
журнала. Пересматривается **первым**, как только что-то проскочило.
|
||||||
|
|
||||||
|
## Журнал дефектов
|
||||||
|
|
||||||
|
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
|
||||||
|
временем теряется не факт, а причина непоймания.
|
||||||
|
|
||||||
|
Форма:
|
||||||
|
|
||||||
|
## ГГГГ-ММ-ДД — краткое последствие [проскочил|пойман]
|
||||||
|
|
||||||
|
- **Где:** файл:строка
|
||||||
|
- **Симптом:** как обнаружилось
|
||||||
|
- **Чем воспроизведён:** тест, команда, замер
|
||||||
|
- **Почему не поймали:** только для проскочивших
|
||||||
|
- **Что меняем:** правило прохода, шаг гейта, конвенция — либо «ничего, цена
|
||||||
|
поимки выше цены дефекта»
|
||||||
|
```
|
||||||
|
|
||||||
|
Новый проект: «Дефектов пока не было. Настройка конвейера появится с первым
|
||||||
|
ревью.»
|
||||||
|
|
||||||
|
## `docs/.pm.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"canon": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Плюс `"migrations": "<путь>"`, если есть БД, и `"tasks": {"sections": [...]}`,
|
||||||
|
если секции беклога отличаются от умолчания.
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Проверка раскладки документов проекта против канона av-dev.
|
||||||
|
|
||||||
|
Определение канона — references/canon.md рядом со скриптом. Здесь только
|
||||||
|
механизируемая часть: пути, лишние файлы, битые ссылки, версия, плейсхолдеры,
|
||||||
|
маркеры долга и две сверки с кодом. Смысловые дубли и оставшееся в архитектуре
|
||||||
|
поведение судит агент — скрипт об этом говорит вслух в конце отчёта.
|
||||||
|
|
||||||
|
Коды выхода — тот же словарь, что у tasks.py:
|
||||||
|
0 сошлось
|
||||||
|
1 дрейф раскладки (рабочая ситуация, чинится)
|
||||||
|
2 ошибка употребления
|
||||||
|
3 окружение: не тот каталог, битый конфиг
|
||||||
|
4 внутренний сбой
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CANON_VERSION = 1
|
||||||
|
|
||||||
|
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
|
# --- Раскладка канона -------------------------------------------------------
|
||||||
|
|
||||||
|
# Обязательные файлы: путь → на какой вопрос отвечает (для внятного отказа).
|
||||||
|
REQUIRED = {
|
||||||
|
"CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта",
|
||||||
|
"docs/.pm.json": "версия канона и пути, нужные проверкам",
|
||||||
|
"docs/passport.md": "зачем и для кого, чем НЕ является",
|
||||||
|
"docs/architecture.md": "как сложено — обзор, окружение, эксплуатация",
|
||||||
|
"docs/security.md": "периметр, недоверенный вход, что вне модели",
|
||||||
|
"docs/review.md": "настройка конвейера + журнал дефектов",
|
||||||
|
"docs/conventions/README.md": "индекс конвенций, правило промоута, что механизировано",
|
||||||
|
"docs/research/README.md": "как снималось, индекс наблюдений",
|
||||||
|
"docs/adr/README.md": "индекс записей, статусы, правило замены",
|
||||||
|
"docs/adr/template.md": "шаблон записи ADR",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Обязателен только при условии: путь → (ключ .pm.json, пояснение).
|
||||||
|
CONDITIONAL = {
|
||||||
|
"docs/database.md": ("migrations", "схема хранилища и настройки"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Что вообще разрешено лежать в docs/ верхним уровнем.
|
||||||
|
ALLOWED_FILES = {
|
||||||
|
".pm.json",
|
||||||
|
"passport.md",
|
||||||
|
"architecture.md",
|
||||||
|
"database.md",
|
||||||
|
"security.md",
|
||||||
|
"review.md",
|
||||||
|
}
|
||||||
|
ALLOWED_DIRS = {"conventions", "research", "adr", "tasks"}
|
||||||
|
|
||||||
|
# Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое.
|
||||||
|
RETIRED = {
|
||||||
|
"review-brief.md": "документы канона и есть бриф; остаток — в review.md",
|
||||||
|
"review-journal.md": "→ docs/review.md",
|
||||||
|
"plan.md": "→ docs/tasks/PLAN.md",
|
||||||
|
"conventions.md": "→ docs/conventions/",
|
||||||
|
"local-research.md": "→ docs/research/",
|
||||||
|
"research.md": "→ docs/research/",
|
||||||
|
"specs": "поведение → openspec/specs/, обзор → docs/architecture.md",
|
||||||
|
"drafts": "идея → задача [idea], отказ → ADR, порядок → PLAN.md",
|
||||||
|
"backlog": "→ docs/tasks/",
|
||||||
|
"review": "→ docs/review.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
|
||||||
|
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
|
||||||
|
MD_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
|
||||||
|
FENCE = re.compile(r"^\s*(```|~~~)")
|
||||||
|
|
||||||
|
|
||||||
|
def strip_code(text: str) -> str:
|
||||||
|
"""Выкинуть блоки кода: пути в примерах и шаблонах — не ссылки, и краснеть
|
||||||
|
на них значит краснеть на каждом образце документа."""
|
||||||
|
out, inside = [], False
|
||||||
|
for line in text.splitlines():
|
||||||
|
if FENCE.match(line):
|
||||||
|
inside = not inside
|
||||||
|
continue
|
||||||
|
out.append("" if inside else line)
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Report:
|
||||||
|
errors: list[str] = field(default_factory=list)
|
||||||
|
notes: list[str] = field(default_factory=list)
|
||||||
|
debts: list[str] = field(default_factory=list)
|
||||||
|
skipped: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def error(self, msg: str) -> None:
|
||||||
|
self.errors.append(msg)
|
||||||
|
|
||||||
|
def note(self, msg: str) -> None:
|
||||||
|
self.notes.append(msg)
|
||||||
|
|
||||||
|
def debt(self, msg: str) -> None:
|
||||||
|
self.debts.append(msg)
|
||||||
|
|
||||||
|
def skip(self, msg: str) -> None:
|
||||||
|
self.skipped.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def fail(code: int, msg: str) -> None:
|
||||||
|
print(f"ОТКАЗ: {msg}", file=sys.stderr)
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(root: Path, rep: Report) -> dict:
|
||||||
|
path = root / "docs" / ".pm.json"
|
||||||
|
if not path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
fail(ENV, f"docs/.pm.json не разбирается: {exc}")
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
fail(ENV, "docs/.pm.json должен быть объектом")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# --- Проверки ---------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def check_version(root: Path, cfg: dict, rep: Report) -> None:
|
||||||
|
if not (root / "docs" / ".pm.json").exists():
|
||||||
|
return # об отсутствии файла скажет check_required, второй раз не нужно
|
||||||
|
if "canon" not in cfg:
|
||||||
|
rep.error("в docs/.pm.json нет ключа canon — версия канона не объявлена")
|
||||||
|
return
|
||||||
|
got = cfg["canon"]
|
||||||
|
if not isinstance(got, int):
|
||||||
|
rep.error(f"canon в docs/.pm.json должен быть целым числом, а не {got!r}")
|
||||||
|
return
|
||||||
|
if got < CANON_VERSION:
|
||||||
|
rep.error(
|
||||||
|
f"проект приведён к канону версии {got}, текущая — {CANON_VERSION}: "
|
||||||
|
f"нужен canon upgrade"
|
||||||
|
)
|
||||||
|
elif got > CANON_VERSION:
|
||||||
|
rep.error(
|
||||||
|
f"проект приведён к канону версии {got}, а скрипт знает {CANON_VERSION}: "
|
||||||
|
f"устарел плагин, обнови маркетплейс"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_required(root: Path, cfg: dict, rep: Report) -> None:
|
||||||
|
for rel, what in REQUIRED.items():
|
||||||
|
if not (root / rel).exists():
|
||||||
|
rep.error(f"нет {rel} — {what}")
|
||||||
|
for rel, (key, what) in CONDITIONAL.items():
|
||||||
|
if key in cfg and not (root / rel).exists():
|
||||||
|
rep.error(f"нет {rel} — {what} (обязателен: в .pm.json объявлен {key})")
|
||||||
|
elif key not in cfg and not (root / rel).exists():
|
||||||
|
rep.skip(f"{rel} — в .pm.json нет ключа {key}, проверка неприменима")
|
||||||
|
|
||||||
|
|
||||||
|
def check_stray(root: Path, rep: Report) -> None:
|
||||||
|
docs = root / "docs"
|
||||||
|
if not docs.is_dir():
|
||||||
|
rep.error("нет каталога docs/")
|
||||||
|
return
|
||||||
|
for entry in sorted(docs.iterdir()):
|
||||||
|
name = entry.name
|
||||||
|
if name in RETIRED:
|
||||||
|
rep.error(f"docs/{name} — слота нет в каноне: {RETIRED[name]}")
|
||||||
|
continue
|
||||||
|
if entry.is_dir():
|
||||||
|
if name not in ALLOWED_DIRS:
|
||||||
|
rep.error(f"docs/{name}/ — каталог вне канона")
|
||||||
|
elif name not in ALLOWED_FILES:
|
||||||
|
rep.error(f"docs/{name} — файл вне канона")
|
||||||
|
|
||||||
|
|
||||||
|
def canon_docs(root: Path) -> list[Path]:
|
||||||
|
"""Документы канона. Каталог задач ведёт tasks.py; упразднённые каталоги
|
||||||
|
уже названы отдельной строкой, и их внутренние ссылки не наша забота —
|
||||||
|
они переезжают целиком."""
|
||||||
|
out = []
|
||||||
|
docs = root / "docs"
|
||||||
|
skip = {"tasks"} | {name for name in RETIRED if not name.endswith(".md")}
|
||||||
|
if docs.is_dir():
|
||||||
|
for path in sorted(docs.rglob("*.md")):
|
||||||
|
head = path.relative_to(docs).parts[0]
|
||||||
|
if head in skip or head in RETIRED:
|
||||||
|
continue
|
||||||
|
out.append(path)
|
||||||
|
claude = root / "CLAUDE.md"
|
||||||
|
if claude.exists():
|
||||||
|
out.append(claude)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def check_links(root: Path, rep: Report) -> None:
|
||||||
|
for path in canon_docs(root):
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
rep.error(f"{path.relative_to(root)} не читается: {exc}")
|
||||||
|
continue
|
||||||
|
for target in MD_LINK.findall(strip_code(text)):
|
||||||
|
target = target.strip()
|
||||||
|
if not target or target.startswith(("http://", "https://", "#", "mailto:")):
|
||||||
|
continue
|
||||||
|
clean = target.split("#", 1)[0]
|
||||||
|
if not clean:
|
||||||
|
continue
|
||||||
|
if (path.parent / clean).exists():
|
||||||
|
continue
|
||||||
|
rep.error(f"{path.relative_to(root)}: битая ссылка на {target}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_placeholders_and_debt(root: Path, rep: Report) -> None:
|
||||||
|
for path in canon_docs(root):
|
||||||
|
text = strip_code(path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
rel = path.relative_to(root)
|
||||||
|
for what in PLACEHOLDER.findall(text):
|
||||||
|
rep.error(f"{rel}: плейсхолдер шаблона не заполнен — {what}")
|
||||||
|
for what in DEBT_MARKER.findall(text):
|
||||||
|
rep.debt(f"{rel}: {what}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_capabilities(root: Path, rep: Report) -> None:
|
||||||
|
specs = root / "openspec" / "specs"
|
||||||
|
arch = root / "docs" / "architecture.md"
|
||||||
|
if not specs.is_dir():
|
||||||
|
rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима")
|
||||||
|
return
|
||||||
|
if not arch.exists():
|
||||||
|
return
|
||||||
|
text = arch.read_text(encoding="utf-8", errors="replace")
|
||||||
|
missing = [d.name for d in sorted(specs.iterdir()) if d.is_dir() and d.name not in text]
|
||||||
|
for name in missing:
|
||||||
|
rep.error(
|
||||||
|
f"capability {name} есть в openspec/specs/, но не упомянута в "
|
||||||
|
f"docs/architecture.md — обзор отстал от нормативных спек"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def changed_files(root: Path, base: str, rep: Report) -> list[str] | None:
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
["git", "-C", str(root), "diff", "--name-only", f"{base}...HEAD"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||||
|
rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})")
|
||||||
|
return None
|
||||||
|
return [line for line in out.stdout.splitlines() if line]
|
||||||
|
|
||||||
|
|
||||||
|
def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None:
|
||||||
|
migrations = cfg.get("migrations")
|
||||||
|
if not migrations:
|
||||||
|
rep.skip("в .pm.json нет ключа migrations — сверка со схемой неприменима")
|
||||||
|
return
|
||||||
|
if not base:
|
||||||
|
rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась")
|
||||||
|
return
|
||||||
|
changed = changed_files(root, base, rep)
|
||||||
|
if changed is None:
|
||||||
|
return
|
||||||
|
touched = [f for f in changed if f.startswith(migrations.rstrip("/") + "/")]
|
||||||
|
if not touched:
|
||||||
|
return
|
||||||
|
if "docs/database.md" not in changed:
|
||||||
|
rep.error(
|
||||||
|
f"миграции изменены ({len(touched)} файлов), а docs/database.md — нет: "
|
||||||
|
f"схема в документации отстала"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_tasks(root: Path, rep: Report) -> None:
|
||||||
|
tasks = root / "docs" / "tasks"
|
||||||
|
if not tasks.is_dir():
|
||||||
|
rep.error("нет docs/tasks/ — каталог задач часть канона")
|
||||||
|
return
|
||||||
|
script = Path(__file__).resolve().parents[2] / "tasks" / "scripts" / "tasks.py"
|
||||||
|
if not script.exists():
|
||||||
|
rep.skip(f"tasks.py не найден по пути {script} — согласованность задач не проверена")
|
||||||
|
return
|
||||||
|
proc = subprocess.run(
|
||||||
|
[sys.executable, str(script), "check", "--dir", str(tasks)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if proc.returncode == 0:
|
||||||
|
return
|
||||||
|
if proc.returncode == 1:
|
||||||
|
rep.error("tasks.py check нашёл дрейф в docs/tasks/ — разбирать его командой tasks.py")
|
||||||
|
else:
|
||||||
|
rep.error(f"tasks.py check отказал с кодом {proc.returncode}: {proc.stderr.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Отчёт ------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def report(rep: Report) -> int:
|
||||||
|
for msg in rep.errors:
|
||||||
|
print(f"ДРЕЙФ {msg}")
|
||||||
|
for msg in rep.notes:
|
||||||
|
print(f"ЗАМЕЧАНИЕ {msg}")
|
||||||
|
if rep.debts:
|
||||||
|
print(f"\nДОЛГ ({len(rep.debts)} маркеров, гейт от них не краснеет):")
|
||||||
|
for msg in rep.debts:
|
||||||
|
print(f" {msg}")
|
||||||
|
if rep.skipped:
|
||||||
|
print("\nНЕ ПРОВЕРЯЛОСЬ:")
|
||||||
|
for msg in rep.skipped:
|
||||||
|
print(f" {msg}")
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\nМашина проверила раскладку, ссылки, версию и две сверки с кодом.\n"
|
||||||
|
"Смысловые дубли, оставшееся в architecture.md поведение и достаточность\n"
|
||||||
|
"честной строки в пустом слоте она не проверяет — это суждение агента."
|
||||||
|
)
|
||||||
|
if rep.errors:
|
||||||
|
print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.")
|
||||||
|
return DRIFT
|
||||||
|
print("\nИтог: канон соблюдён в механизируемой части.")
|
||||||
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_check(args: argparse.Namespace) -> int:
|
||||||
|
root = Path(args.dir).resolve()
|
||||||
|
if not root.is_dir():
|
||||||
|
fail(ENV, f"каталог {root} не найден")
|
||||||
|
if not (root / "docs").exists() and not (root / "CLAUDE.md").exists():
|
||||||
|
fail(ENV, f"{root} не похож на корень проекта: нет ни docs/, ни CLAUDE.md")
|
||||||
|
|
||||||
|
rep = Report()
|
||||||
|
cfg = read_config(root, rep)
|
||||||
|
check_version(root, cfg, rep)
|
||||||
|
check_required(root, cfg, rep)
|
||||||
|
check_stray(root, rep)
|
||||||
|
check_links(root, rep)
|
||||||
|
check_placeholders_and_debt(root, rep)
|
||||||
|
check_capabilities(root, rep)
|
||||||
|
check_migrations(root, cfg, args.base, rep)
|
||||||
|
check_tasks(root, rep)
|
||||||
|
return report(rep)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_version(args: argparse.Namespace) -> int:
|
||||||
|
root = Path(args.dir).resolve()
|
||||||
|
cfg = read_config(root, Report())
|
||||||
|
got = cfg.get("canon", "не объявлена")
|
||||||
|
print(f"канон скрипта: {CANON_VERSION}")
|
||||||
|
print(f"канон проекта: {got}")
|
||||||
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="docs.py",
|
||||||
|
description="механическая проверка канона документов проекта",
|
||||||
|
)
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
p_check = sub.add_parser("check", help="раскладка, ссылки, версия, сверки с кодом")
|
||||||
|
p_check.add_argument("--dir", default=".", help="корень проекта (по умолчанию текущий)")
|
||||||
|
p_check.add_argument("--base", default=None, help="база диффа для сверки миграций")
|
||||||
|
p_check.set_defaults(func=cmd_check)
|
||||||
|
|
||||||
|
p_ver = sub.add_parser("version", help="версия канона скрипта и проекта")
|
||||||
|
p_ver.add_argument("--dir", default=".", help="корень проекта")
|
||||||
|
p_ver.set_defaults(func=cmd_version)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
return args.func(args)
|
||||||
|
except SystemExit:
|
||||||
|
raise
|
||||||
|
except Exception as exc: # noqa: BLE001 — последний рубеж, код 4 по словарю
|
||||||
|
print(f"ВНУТРЕННИЙ СБОЙ: {exc}", file=sys.stderr)
|
||||||
|
return INTERNAL
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
---
|
||||||
|
name: docs
|
||||||
|
description: Вести содержимое документов канона по ходу разработки — синк после сделанной задачи с построчным отчётом по каждому документу, заведение ADR промоутом из архивного design.md, запись наблюдения в research, запись дефекта и настройки конвейера в review.md, чистка architecture.md от поведения с маркерами долга. Использовать, когда задача сделана и надо обновить документацию, когда просят завести ADR или записать решение, занести находку о внешних данных, записать проскочивший дефект, разгрузить разросшуюся архитектуру. Раскладку и соответствие канону проверяет скилл canon.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ведение содержимого канона
|
||||||
|
|
||||||
|
Скилл владеет **содержимым** документов канона; раскладкой владеет `canon`.
|
||||||
|
Определение канона и роли документов — [канон](../canon/references/canon.md),
|
||||||
|
здесь не пересказывается.
|
||||||
|
|
||||||
|
Главный вызывающий — **шаг синка документации в пайплайне задачи**. Пайплайн
|
||||||
|
живёт в другом плагине и зовёт этот скилл по имени; проект без пайплайна ведёт
|
||||||
|
документацию тем же скиллом вручную.
|
||||||
|
|
||||||
|
## Правило, из которого всё следует
|
||||||
|
|
||||||
|
**Принуждённое отрицание.** Синк обязан назвать **каждый** документ канона: либо
|
||||||
|
чем он обновлён, либо «не требуется, потому что…». Нетронутые группируются одной
|
||||||
|
строкой с общей причиной.
|
||||||
|
|
||||||
|
Причина, по которой правило именно такое, измерена: у ADR был список триггеров
|
||||||
|
прозой — и он дал **6 записей на 43 изменения**. Прозаический триггер, который
|
||||||
|
некому проверить, не срабатывает. Умолчание «не написал» становится отличимым от
|
||||||
|
«написал, что не требуется», только когда отрицание обязательно.
|
||||||
|
|
||||||
|
Это тот же приём, что «границы покрытия» в отчёте ревью и «пустое называется
|
||||||
|
пустым» в каноне.
|
||||||
|
|
||||||
|
## Чек-лист синка
|
||||||
|
|
||||||
|
Идёт сверху вниз; каждая строка попадает в доклад.
|
||||||
|
|
||||||
|
| Документ | Обновляется, когда | Проверка |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `openspec/specs/` | всегда при изменении поведения | вливает `opsx:archive` |
|
||||||
|
| `database.md` | тронуты миграции | `docs.py check --base` |
|
||||||
|
| `architecture.md` | новый компонент, граница, внешняя зависимость, изменилось окружение | `docs.py`: capability без упоминания |
|
||||||
|
| `adr/` | дорогой откат, намеренный отказ, пересмотр прежнего | нет — только этот чек-лист |
|
||||||
|
| `research/` | узнали новое о внешнем формате или данных | нет |
|
||||||
|
| `security.md` | новый недоверенный вход, токен, путь наружу, сдвиг периметра | нет |
|
||||||
|
| `conventions/` | находка принята и не специфична для одного места | промоут |
|
||||||
|
| `review.md` | дефект воспроизведён; сузили или расширили проверку | нет |
|
||||||
|
| `passport.md` | новый потребитель, сдвиг границы «чем не является» | нет |
|
||||||
|
| `CLAUDE.md` | изменился инвариант, гейт, запрет, необратимое | нет |
|
||||||
|
|
||||||
|
Пример доклада:
|
||||||
|
|
||||||
|
```
|
||||||
|
Синк документации:
|
||||||
|
- architecture.md — добавлен воркер свёртки, ссылка на capability reindex
|
||||||
|
- database.md — миграция 00006, таблица bucket
|
||||||
|
- adr/ — заведён ADR-2026-08-03-ochered-tablicej: отказ от внешней очереди
|
||||||
|
- research/ — новое о формате не узнано
|
||||||
|
- passport, security, conventions, review — не требуется: изменение внутреннее
|
||||||
|
```
|
||||||
|
|
||||||
|
## ADR — промоут, а не второе сочинение
|
||||||
|
|
||||||
|
Обоснование уже написано: `opsx:propose` кладёт `design.md` в каждый change, и
|
||||||
|
после архивации он лежит в `openspec/changes/archive/<id>/design.md` с разделами
|
||||||
|
`Context` / `Goals / Non-Goals` / `Decisions` / `Risks / Trade-offs`.
|
||||||
|
|
||||||
|
**ADR цитирует решение оттуда и ссылается на источник.** Не пересказывает и не
|
||||||
|
сочиняет заново.
|
||||||
|
|
||||||
|
Заводится, когда верно одно из трёх:
|
||||||
|
|
||||||
|
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||||||
|
- **намеренный отказ** от очевидного подхода — чтобы не переоткрывать «а почему
|
||||||
|
мы не сделали X»;
|
||||||
|
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
|
||||||
|
`заменено на ADR-…`, а у новой в контексте строка «Заменяет ADR-…».
|
||||||
|
|
||||||
|
Не заводится для рутины и для того, что видно из кода и `git log`.
|
||||||
|
|
||||||
|
Порядок: имя `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение **принято**, слаг
|
||||||
|
английский; тело по `docs/adr/template.md`; строка в индексе `docs/adr/README.md`
|
||||||
|
сверху. Активная запись статуса не имеет.
|
||||||
|
|
||||||
|
## Чистка `architecture.md`
|
||||||
|
|
||||||
|
Обзор не держит поведение — его нормативный дом `openspec/specs/`. Раздел, где
|
||||||
|
поведение осталось, помечается маркером долга:
|
||||||
|
|
||||||
|
```
|
||||||
|
<!-- канон: поведение → openspec/specs/<capability> -->
|
||||||
|
```
|
||||||
|
|
||||||
|
`docs.py` считает маркеры и печатает числом; **гейт от них не краснеет** — это
|
||||||
|
долг, а не отказ, иначе постепенный переезд стал бы невозможен.
|
||||||
|
|
||||||
|
Разбирается порциями: раздел вычищается той задачей, которая его касается.
|
||||||
|
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
|
||||||
|
обоснование в ADR, обзор остаётся строкой со ссылкой на capability.
|
||||||
|
|
||||||
|
## Запись в `research/`
|
||||||
|
|
||||||
|
Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата
|
||||||
|
расходится с практикой. **Число — с провенансом**: команда или условия, которыми
|
||||||
|
получено, чтобы его можно было перепроверить.
|
||||||
|
|
||||||
|
Число без источника проход обязан читать как условие. Число, чей источник по
|
||||||
|
ссылке не подтвердился, **не переписывается по догадке** — остаётся с пометкой
|
||||||
|
«расходится с источником: там <что нашли>». Молча подставить «правильное» число
|
||||||
|
хуже всего: расхождение перестанет быть видно, а причина останется.
|
||||||
|
|
||||||
|
## Запись в `review.md`
|
||||||
|
|
||||||
|
Два раздела с разными сроками жизни, и путать их нельзя.
|
||||||
|
|
||||||
|
**Журнал дефектов.** Запись на каждый воспроизведённый дефект с пометкой
|
||||||
|
**проскочил / пойман ревью**. Пишется сразу, а не ретроспективно: со временем
|
||||||
|
теряется не факт, а причина непоймания — единственное, ради чего журнал есть.
|
||||||
|
Форма: где, симптом, чем воспроизведён, почему не поймали (для проскочивших),
|
||||||
|
что меняем. Вывод «ничего не меняем, цена поимки выше цены дефекта» — законный
|
||||||
|
исход.
|
||||||
|
|
||||||
|
**Настройка конвейера.** Типовые узлы; типовые ложноположительные; вопросы к
|
||||||
|
проходам поимённо с провенансом; недоступно проверке. Последний раздел делится
|
||||||
|
на «не проверит ни один проход» (принципиальная граница, по факту промаха не
|
||||||
|
пересматривается) и «перестали проверять сознательно» — этот **пересматривается
|
||||||
|
первым**, как только что-то проскочило.
|
||||||
|
|
||||||
|
## Промоут в конвенции
|
||||||
|
|
||||||
|
Находка → конвенция → правило линтера → **удаление из прозы**. Процедура
|
||||||
|
принадлежит конвейеру ревью и живёт в его `references/promote.md`; здесь только
|
||||||
|
то, что касается документа:
|
||||||
|
|
||||||
|
- формулировка — **проверяемое свойство**, а не совет;
|
||||||
|
- в прозе остаётся только то, что принципиально не выражается правилом;
|
||||||
|
- как только правило работает, формулировка из `conventions/<тема>.md`
|
||||||
|
**удаляется**, а правило попадает в перечень механизированного в
|
||||||
|
`conventions/README.md` со ссылкой на место механизации.
|
||||||
|
|
||||||
|
Непойманное место механизации означает, что проход по конвенциям будет
|
||||||
|
добросовестно проверять уже проверенное.
|
||||||
|
|
||||||
|
## Чего этот скилл не делает
|
||||||
|
|
||||||
|
- **Не проверяет раскладку** — это `canon`.
|
||||||
|
- **Не заводит недостающие документы** — их скелет кладёт `canon adopt` или
|
||||||
|
`init`.
|
||||||
|
- **Не сочиняет содержание.** Нечего записать — так и пишется, честной строкой.
|
||||||
|
- **Не переоформляет документы «заодно»**: правится то, чего коснулась работа.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
name: init
|
||||||
|
description: Завести новый проект — сессия вопросов и ответов по свободному описанию замысла, из которой рождается первичная документация по канону av-dev: паспорт, CLAUDE.md с инвариантами и командами, модель угроз с периметром, первые цели в плане и скелет остальных документов. Использовать, когда начинают новый проект с нуля, когда есть только текст «что мне нужно и почему» и надо превратить его в рабочую документацию, когда просят провести стартовое интервью по брифу. Проект, где документация уже как-то ведётся, переводит скилл canon.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Заведение нового проекта
|
||||||
|
|
||||||
|
Вход — свободный текст «что мне нужно и почему». Выход — канон документов, с
|
||||||
|
которого дальше работают все остальные скиллы.
|
||||||
|
|
||||||
|
**Определение канона — [канон](../canon/references/canon.md).** Читается до
|
||||||
|
первого вопроса: интервью идёт по слотам канона, а не по вкусу.
|
||||||
|
|
||||||
|
## Что `init` физически не может произвести
|
||||||
|
|
||||||
|
В новом репозитории **нет кода**, а `architecture.md`, `database.md`,
|
||||||
|
`conventions/` и `research/` выводятся из него. Их сочинение на старте — это
|
||||||
|
проектирование вперёд реальности, и оно протухнет раньше первой задачи.
|
||||||
|
|
||||||
|
Поэтому `init` заполняет то, что человек знает **до первой строки кода**:
|
||||||
|
|
||||||
|
| Заполняется | Остаётся скелетом с честной строкой |
|
||||||
|
| --- | --- |
|
||||||
|
| `passport.md` | `architecture.md` |
|
||||||
|
| `CLAUDE.md` | `database.md` |
|
||||||
|
| `security.md` | `conventions/` |
|
||||||
|
| `docs/tasks/PLAN.md` — первые цели | `research/`, `adr/` |
|
||||||
|
| `docs/.pm.json` | `review.md` — журнал пуст, настройка появится с первым ревью |
|
||||||
|
|
||||||
|
Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет,
|
||||||
|
заводится первой задачей». Проход читает её как факт.
|
||||||
|
|
||||||
|
## Порядок интервью — зависимость, а не удобство
|
||||||
|
|
||||||
|
Каждый блок опирается на ответ предыдущего; переставлять нельзя.
|
||||||
|
|
||||||
|
1. **Цель и потребители.** Ради чего это; кто пользуется — список закрытый, и
|
||||||
|
он определяет, что считать нужным, а что интересным.
|
||||||
|
2. **Чем это НЕ является и мера успеха.** Граница домена — критерий, по
|
||||||
|
которому архитектурный проход потом судит о переносе понятия. Мера — по чему
|
||||||
|
поймём, что удалось.
|
||||||
|
3. **Периметр и недоверенный вход.** Открыт наружу или контур доверенный; что
|
||||||
|
приходит извне и каким каналом; что чувствительнее чего. Контур ещё не
|
||||||
|
развёрнут — назови **оба** периметра, целевой и сегодняшний.
|
||||||
|
4. **Стек, хранилище, необратимое.** Чем пишем и почему; где данные; что в этом
|
||||||
|
проекте нельзя откатить — деплой, выкладка наружу, перезапись данных.
|
||||||
|
5. **Чем краснеет гейт.** Какие проверки обязательны; что красит безусловно;
|
||||||
|
чего в гейте намеренно не будет и кто тогда это гоняет.
|
||||||
|
6. **Первые цели.** Направления, а не задачи: три-пять целей линии с
|
||||||
|
обоснованием порядка прозой.
|
||||||
|
|
||||||
|
### Как вести
|
||||||
|
|
||||||
|
- **Не больше трёх вопросов за итерацию** (`AskUserQuestion`), рекомендация
|
||||||
|
первым вариантом. Между итерациями применяй уже решённое.
|
||||||
|
- **Сперва вычитай ответы из брифа.** Вопрос, ответ на который в тексте уже
|
||||||
|
есть, задавать не надо — покажи своё прочтение и спроси, верно ли.
|
||||||
|
- **Не выдумывай четыре вещи:** периметр, что необратимо, измеренные числа и
|
||||||
|
адресата дорогой проверки. Их из замысла не вывести. Не сказано — пиши
|
||||||
|
«неизвестно» с пометкой, что ждёт ответа.
|
||||||
|
- **Развилка замысла — человеку, механика — сама.** Имена файлов, слаги, порядок
|
||||||
|
строк не выносятся.
|
||||||
|
|
||||||
|
## Порядок работы
|
||||||
|
|
||||||
|
1. Прочитай бриф целиком. Выпиши, на какие блоки интервью ответ уже есть.
|
||||||
|
2. Проведи интервью итерациями по ≤3 вопроса.
|
||||||
|
3. Заведи `docs/.pm.json` с текущей версией канона.
|
||||||
|
4. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и
|
||||||
|
отдельным файлом не остаётся: два дома для одного замысла разойдутся на
|
||||||
|
первом же уточнении.
|
||||||
|
5. Заведи скелет остальных — каждый с честной строкой.
|
||||||
|
6. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет
|
||||||
|
форматом целей и задач.
|
||||||
|
7. `docs.py check` из скилла `canon` — до зелёного в механизируемой части.
|
||||||
|
8. Покажи человеку, что получилось, и **отдельным списком** — что выведено из
|
||||||
|
брифа, что предположено, что осталось неизвестным. Правят по этим строкам.
|
||||||
|
|
||||||
|
## Что дальше
|
||||||
|
|
||||||
|
- Содержимое канона по ходу разработки ведёт скилл `docs`.
|
||||||
|
- Раскладку проверяет `canon check`.
|
||||||
|
- Первую задачу берёт пайплайн проекта; `architecture.md` и `conventions/`
|
||||||
|
наполняются его шагом синка, а не заранее.
|
||||||
|
|
||||||
|
## Чего этот скилл не делает
|
||||||
|
|
||||||
|
- **Не проектирует систему.** Архитектура выводится из кода, а не наоборот.
|
||||||
|
- **Не пишет код** и не заводит сборку.
|
||||||
|
- **Не переводит существующий проект** — это `canon adopt`. Признак: в
|
||||||
|
репозитории уже есть документация или беклог в какой-то раскладке.
|
||||||
|
- **Не решает за человека**, что важно: цель, границы и периметр — его ответы.
|
||||||
@@ -157,8 +157,21 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
|||||||
|
|
||||||
## Стимулы, которые процесс создаёт
|
## Стимулы, которые процесс создаёт
|
||||||
|
|
||||||
Правило, которое можно обойти в свою пользу, будет обойдено. Известные обходы и
|
Правило, которое можно обойти в свою пользу, будет обойдено.
|
||||||
защиты:
|
|
||||||
|
**Приёмщик и исполнитель здесь совпадают, и это надо назвать вслух.** Задачу
|
||||||
|
закрывает и двигает по индексам агент-оркестратор — тот же, кто её и сделал.
|
||||||
|
Прежде границу держала механика: моста между плагинами не было, и закрыть задачу
|
||||||
|
пайплайн физически не мог. Теперь мост есть, и защита у трёх обходов ниже —
|
||||||
|
**только текстовая**. Опоры, которые остались настоящими:
|
||||||
|
|
||||||
|
- **отчёт триажа** в `openspec/changes/<id>/review/` — независимый артефакт,
|
||||||
|
написанный ревью, а не исполнителем; по нему сверяют состав прогона и урожай;
|
||||||
|
- **`SPRINT.md` под git** — `git log -p` показывает, что и когда было закрыто;
|
||||||
|
- **`reopen <слаг> --reason`** — закрытие не окончательно. Приёмка человеком на
|
||||||
|
сессии его отменяет, и это штатная операция, а не скандал.
|
||||||
|
|
||||||
|
Известные обходы:
|
||||||
|
|
||||||
- **Скрыть блокер** — он останавливает всё и выглядит как провал исполнителя.
|
- **Скрыть блокер** — он останавливает всё и выглядит как провал исполнителя.
|
||||||
Защита: тест про остаток плюс прямая запись, что **объявление блокера
|
Защита: тест про остаток плюс прямая запись, что **объявление блокера
|
||||||
@@ -166,13 +179,16 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
|||||||
- **Не записать вопрос** на задаче-кандидате, чтобы не вычеркнуть её из
|
- **Не записать вопрос** на задаче-кандидате, чтобы не вычеркнуть её из
|
||||||
ближайшего набора. Защита: вопросы кандидатов разбираются на той же сессии
|
ближайшего набора. Защита: вопросы кандидатов разбираются на той же сессии
|
||||||
**вне очереди порции**.
|
**вне очереди порции**.
|
||||||
- **Занизить критерии приёмки**, раз они пол. Защита: расхождение критериев с
|
- **Занизить критерии приёмки**, раз они пол. Защита ослаблена: правит их тот же,
|
||||||
сутью — дефект критериев, правит их приёмщик, а он **не исполнитель**.
|
кто по ним отчитывается. Остаётся требование, что расхождение критериев с
|
||||||
- **Сжать задачу до остатка** и отчитаться «сделана». Защита: пол для остатка —
|
сутью — **дефект критериев, о котором сообщают, а не молча дорабатывают**, и
|
||||||
польза, названная в хуке.
|
переоценка на сессии, где критерии видит человек.
|
||||||
|
- **Сжать задачу до остатка** и отчитаться «сделана». Защита ослаблена там же.
|
||||||
|
Пол для остатка — польза, названная в хуке; проверяет его человек при приёмке,
|
||||||
|
и `reopen` — его инструмент.
|
||||||
- **Занизить урожай** — не заводить найденное по ходу. Защита: поимённая сверка
|
- **Занизить урожай** — не заводить найденное по ходу. Защита: поимённая сверка
|
||||||
отчётов ревью со списком заведённого, составленным **не отчитывающимся**:
|
со **сохранённым отчётом триажа**, а не с прозой исполнителя. Каждая
|
||||||
каждая отложенная находка имеет либо слаг, либо строку «не заведена: причина».
|
отложенная находка имеет либо слаг, либо строку «не заведена: причина».
|
||||||
Нулевой урожай при непустом отчёте виден сразу.
|
Нулевой урожай при непустом отчёте виден сразу.
|
||||||
|
|
||||||
Стимулы внутри пайплайна задачи (занизить требования к проверке, пропустить
|
Стимулы внутри пайплайна задачи (занизить требования к проверке, пропустить
|
||||||
@@ -180,27 +196,24 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
|||||||
|
|
||||||
## Слоты проекта
|
## Слоты проекта
|
||||||
|
|
||||||
Сессия не знает ни языка, ни сборки, ни CI. Проект **обязан дописать в
|
Сессия не знает ни языка, ни сборки, ни CI. Часть проектного отвечает
|
||||||
`CLAUDE.md`**:
|
[канон](../canon/references/canon.md) структурой: разбор процесса (шаг 2) живёт
|
||||||
|
в `docs/review.md`, оракулы и «чем краснеет безусловно» — в семантике гейта в
|
||||||
|
`CLAUDE.md`. Остальное проект **дописывает в `CLAUDE.md`**:
|
||||||
|
|
||||||
1. **Пайплайн задачи** — чем задача выполняется и что входит в его определение
|
1. **Пайплайн задачи** — чем задача выполняется и что входит в его определение
|
||||||
готовности. Сессия требует только форму: пайплайн пройден + критерии приёмки
|
готовности. Сессия требует только форму: пайплайн пройден + критерии приёмки
|
||||||
проверены поимённо.
|
проверены поимённо.
|
||||||
2. **Общий станок** — какая проверка, покраснев, врывается в замороженный
|
2. **Общий станок** — какая проверка, покраснев, врывается в замороженный
|
||||||
спринт.
|
спринт.
|
||||||
3. **Необратимое** — что спрашивается у человека всегда.
|
3. **Необратимое** — что спрашивается у человека всегда (тот же слот, что у
|
||||||
4. **Где живёт разбор процесса** (шаг 2): журнал промахов конвейера, ADR или
|
скилла `tasks`; дом один).
|
||||||
раздел документации. Нет такого места — шаг 2 производит его первым же
|
4. **Как критерии приёмки переживают удаление файла задачи** — файл удаляется
|
||||||
заходом, иначе выводы сессии живут один контекст.
|
|
||||||
5. **Как критерии приёмки переживают удаление файла задачи** — файл удаляется
|
|
||||||
при закрытии, поэтому критерии копируются туда, где их увидит приёмщик
|
при закрытии, поэтому критерии копируются туда, где их увидит приёмщик
|
||||||
(предложение об изменении, описание ветки, тело коммита). Куда именно —
|
(предложение об изменении, описание ветки, тело коммита). Куда именно —
|
||||||
решает проект.
|
решает проект.
|
||||||
6. **Ориентир по размеру спринта**, если он замерялся. Умолчание — 5–8 задач, и
|
5. **Ориентир по размеру спринта**, если он замерялся. Умолчание — 5–8 задач, и
|
||||||
это **ориентир, а не закон**.
|
это **ориентир, а не закон**.
|
||||||
7. **Команда учёта задач** — готовая строка вызова `tasks.py` (слот скилла
|
|
||||||
`tasks`). Ею владелец спринта закрывает задачи и заводит урожай; чужой
|
|
||||||
контекст сам путь к плагину не знает и знать не должен.
|
|
||||||
|
|
||||||
Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост
|
Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост
|
||||||
беклога) — предмет шага 2, а не константы этого скилла.
|
беклога) — предмет шага 2, а не константы этого скилла.
|
||||||
+4
-3
@@ -44,9 +44,10 @@
|
|||||||
их не пересматривает конкретный шаг.
|
их не пересматривает конкретный шаг.
|
||||||
|
|
||||||
**Артефакт обязателен.** Вывод, оставшийся в контексте сессии, не существует:
|
**Артефакт обязателен.** Вывод, оставшийся в контексте сессии, не существует:
|
||||||
следующая сессия его не увидит. Куда он пишется — журнал промахов, ADR, раздел
|
следующая сессия его не увидит. Дом у него один и известен из канона —
|
||||||
документации — называет `CLAUDE.md` проекта; нет такого места, значит первый
|
**`docs/review.md`**: вывод про конвейер и про то, что перестали проверять, идёт
|
||||||
разбор его и заводит.
|
в раздел настройки, вывод про воспроизведённый дефект — в журнал. Решение с
|
||||||
|
долгим следом — в `docs/adr/`.
|
||||||
|
|
||||||
Отдельным ритуалом ретроспектива не выделяется: процесс личный,
|
Отдельным ритуалом ретроспектива не выделяется: процесс личный,
|
||||||
синхронизировать некого.
|
синхронизировать некого.
|
||||||
@@ -41,8 +41,12 @@ description: Ведение задач и целей как каталога mar
|
|||||||
|
|
||||||
## Раскладка
|
## Раскладка
|
||||||
|
|
||||||
|
Каталог задач — **`docs/tasks`, жёстко**: это часть
|
||||||
|
[канона документов](../canon/references/canon.md), и подгоняется под него
|
||||||
|
проект, а не наоборот.
|
||||||
|
|
||||||
```
|
```
|
||||||
<tasks>/ по умолчанию docs/tasks, путь настраивается
|
docs/tasks/
|
||||||
items/ задачи и цели файлами, <slug>.md, слаги английские
|
items/ задачи и цели файлами, <slug>.md, слаги английские
|
||||||
PLAN.md оглавление целей: линия (упорядоченная) и кусты
|
PLAN.md оглавление целей: линия (упорядоченная) и кусты
|
||||||
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
||||||
@@ -70,7 +74,7 @@ description: Ведение задач и целей как каталога mar
|
|||||||
**У сделанной задачи записи не остаётся** — файл и строка удаляются (`close
|
**У сделанной задачи записи не остаётся** — файл и строка удаляются (`close
|
||||||
--implemented`). Ей хватает коммита и документации проекта; вторая запись была
|
--implemented`). Ей хватает коммита и документации проекта; вторая запись была
|
||||||
бы вторым домом для того же факта. Вопрос «что было в спринте N» отвечается
|
бы вторым домом для того же факта. Вопрос «что было в спринте N» отвечается
|
||||||
даром: `SPRINT.md` лежит под git, `git log -p <tasks>/SPRINT.md` отдаёт историю
|
даром: `SPRINT.md` лежит под git, `git log -p docs/tasks/SPRINT.md` отдаёт историю
|
||||||
всех наборов без отдельного журнала.
|
всех наборов без отдельного журнала.
|
||||||
|
|
||||||
## Цели
|
## Цели
|
||||||
@@ -100,9 +104,9 @@ description: Ведение задач и целей как каталога mar
|
|||||||
|
|
||||||
## Инструмент (`tasks.py`)
|
## Инструмент (`tasks.py`)
|
||||||
|
|
||||||
Пусть `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`, а `D` — каталог
|
Пусть `tk="$CLAUDE_PLUGIN_ROOT/skills/tasks/scripts/tasks.py"`, а `D` —
|
||||||
задач проекта (см. «Переносимость»; `--dir` опускается только если каталог
|
`docs/tasks` от корня проекта. `--dir` стоит в примерах намеренно: вызов из
|
||||||
лежит в умолчаниях под текущим каталогом).
|
подкаталога — обычное дело.
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 $tk check --dir D # согласованность индексов + здоровье
|
python3 $tk check --dir D # согласованность индексов + здоровье
|
||||||
@@ -116,7 +120,7 @@ python3 $tk close S --dir D --implemented # просто удалить (ре
|
|||||||
python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась
|
python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась
|
||||||
python3 $tk sprint start --goal S --dir D | take S… | drop S… --reason R | close [--dissolve --reason R]
|
python3 $tk sprint start --goal S --dir D | take S… | drop S… --reason R | close [--dissolve --reason R]
|
||||||
python3 $tk init --dir D [--sections …] [--plan-sections …] [--items …] [--backlog …] …
|
python3 $tk init --dir D [--sections …] [--plan-sections …] [--items …] [--backlog …] …
|
||||||
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация чужого репозитория, скилл adopt
|
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md
|
||||||
```
|
```
|
||||||
|
|
||||||
**Коды выхода — единый словарь; на нём ветвятся скиллы, а не на тексте вывода:**
|
**Коды выхода — единый словарь; на нём ветвятся скиллы, а не на тексте вывода:**
|
||||||
@@ -126,7 +130,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
| 0 | сошлось / сделано | дальше по сценарию |
|
| 0 | сошлось / сделано | дальше по сценарию |
|
||||||
| 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать |
|
| 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать |
|
||||||
| 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу |
|
| 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу |
|
||||||
| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `.tasks.json`, повтор не поможет |
|
| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `docs/.pm.json`, повтор не поможет |
|
||||||
| 4 | внутренний сбой | дефект скрипта, доложить |
|
| 4 | внутренний сбой | дефект скрипта, доложить |
|
||||||
|
|
||||||
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
|
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
|
||||||
@@ -220,9 +224,12 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
### Прийти в репозиторий, где задачи уже как-то ведутся
|
### Прийти в репозиторий, где задачи уже как-то ведутся
|
||||||
|
|
||||||
Разовая операция: вывести каталог задач из старой раскладки беклога, `TODO.md`,
|
Разовая операция: вывести каталог задач из старой раскладки беклога, `TODO.md`,
|
||||||
заметок или списка шагов в плане — скилл `adopt`. Сюда же относится
|
заметок или списка шагов в плане — [references/adopt.md](references/adopt.md).
|
||||||
переименование транслитных слагов в английские: оно делается **одним проходом
|
Сюда же относится переименование транслитных слагов в английские: оно делается
|
||||||
вместе с починкой перекрёстных ссылок**, а не по одному слагу.
|
**одним проходом вместе с починкой перекрёстных ссылок**, а не по одному слагу.
|
||||||
|
|
||||||
|
Если переводить надо не только задачи, а весь `docs/` — это скилл
|
||||||
|
`av-dev-pm:canon`, и он зовёт этот сценарий сам на своём шаге.
|
||||||
|
|
||||||
### Декомпозиция и штурм идеи
|
### Декомпозиция и штурм идеи
|
||||||
|
|
||||||
@@ -256,66 +263,47 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего
|
Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего
|
||||||
не знает ни про Go, ни про npm, ни про конкретный багтрекер — задачи для него
|
не знает ни про Go, ни про npm, ни про конкретный багтрекер — задачи для него
|
||||||
просто каталог markdown. Текст задач — русский (язык документации проекта);
|
просто каталог markdown. Текст задач — русский (язык документации проекта);
|
||||||
зашита только латиница слага.
|
зашита только латиница слага. OpenSpec ему тоже не нужен.
|
||||||
|
|
||||||
- **Каталог задач** ищется цепочкой: `--dir` → **указатель в `CLAUDE.md`
|
- **Каталог задач — `docs/tasks`, жёстко.** Цепочки разрешения нет: раскладка
|
||||||
проекта** (его читаешь ты и передаёшь `--dir`; скрипт чужую документацию не
|
канона одинакова во всех проектах, и искать больше нечего. Каталога нет — код
|
||||||
разбирает) → `.tasks.json` вверх от текущего каталога → умолчания
|
3 и вопрос человеку; `init` заводит его **только** когда проект действительно
|
||||||
(`docs/tasks`, `tasks`, `doc/tasks`) вверх от текущего каталога, до корня
|
новый, а перевод чужой раскладки делает `av-dev-pm:canon`.
|
||||||
репозитория. Не нашлось — код 3 и вопрос человеку, а не догадка: `init`
|
- **Настройки живут в `docs/.pm.json`**, ключ `tasks`: секции беклога и имена
|
||||||
заводит каталог **только** когда проект действительно новый.
|
индексов, если они отличаются от умолчания. Один конфиг на весь канон, а не по
|
||||||
**В примерах `--dir` стоит намеренно:** каталог вне умолчаний иначе не
|
одному на каталог.
|
||||||
находится, а вызов из подкаталога — обычное дело.
|
|
||||||
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
|
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
|
||||||
и названия — дело проекта (умолчание `ядро` / `инфра`).
|
и названия — дело проекта (умолчание `ядро` / `инфра`).
|
||||||
- **Имена индексов и подкаталога** — параметры `init`, живут в
|
|
||||||
`<tasks>/.tasks.json`. Ничего не зашито именем файла.
|
|
||||||
|
|
||||||
### Вызов из другого плагина
|
### Вызов из другого плагина
|
||||||
|
|
||||||
`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: чужой
|
`$CLAUDE_PLUGIN_ROOT` раскрывается **только внутри своего плагина**: пайплайн
|
||||||
контекст — пайплайн задачи, конвейер ревью, любой другой скилл — до `tasks.py`
|
задачи, конвейер ревью и любой другой чужой контекст до `tasks.py` по этой
|
||||||
по этой переменной не дотянется. Поэтому контракт такой:
|
переменной не дотянутся. Мост — **вызов скилла через пространство имён**, а не
|
||||||
|
путь:
|
||||||
|
|
||||||
> **Проект называет команду учёта задач в своём `CLAUDE.md`** — целиком, готовой
|
> Чужой контекст зовёт `Skill av-dev-pm:tasks` и называет, что нужно сделать
|
||||||
> к запуску строкой (слот 6 ниже). Вызывающий берёт её оттуда. Слота нет —
|
> («закрой задачу `<слаг>`, реализована»). Скилл разрешает свой
|
||||||
> вызывающий **не выдумывает путь и не правит индекс руками**, а сообщает в
|
> `$CLAUDE_PLUGIN_ROOT` сам. Путь наружу не выносится вовсе.
|
||||||
> докладе, что закрытие/заведение остаётся за владельцем задач.
|
|
||||||
|
|
||||||
Так вызывающему не нужно знать ни про плагин, ни про его расположение: он знает
|
Плагина в проекте нет — вызов не разрешится, и вызывающий **не выдумывает путь и
|
||||||
проект, а проект знает команду.
|
не правит индекс руками**, а сообщает в докладе, что учёт задач остаётся за
|
||||||
|
владельцем.
|
||||||
|
|
||||||
## Слоты проекта
|
## Слоты проекта
|
||||||
|
|
||||||
Скилл не знает ни языка программирования, ни сборки, ни CI, ни трекера — задачи
|
Почти всё, что скиллу нужно знать о проекте, отвечает канон структурой: куда
|
||||||
для него просто каталог markdown. Всё проектное живёт в `CLAUDE.md` проекта, и
|
переезжает суть реализованной задачи — `openspec/specs`, `adr/`, архив change;
|
||||||
**проект обязан дописать туда**:
|
какие в проекте оракулы — семантика гейта в `CLAUDE.md`. Отдельными слотами
|
||||||
|
остаётся то, чего из раскладки не вывести. **Проект дописывает в `CLAUDE.md`**:
|
||||||
|
|
||||||
1. **Путь каталога задач**, если он не `docs/tasks`, и **секции беклога** — по
|
1. **Что такое «сделана»** — чем задача выполняется (пайплайн проекта) и что
|
||||||
умолчанию `ядро` / `инфра`; граница между ними режется по существу работы, а
|
|
||||||
не по её поводу. Имена индексов и подкаталога, если они другие, задаются
|
|
||||||
`init` и живут в `<tasks>/.tasks.json`.
|
|
||||||
2. **Что такое «сделана»** — чем задача выполняется (пайплайн проекта) и что
|
|
||||||
входит в его определение готовности. Скилл требует лишь **форму**: пайплайн
|
входит в его определение готовности. Скилл требует лишь **форму**: пайплайн
|
||||||
проекта пройден + критерии приёмки проверены поимённо.
|
проекта пройден + критерии приёмки проверены поимённо.
|
||||||
3. **Куда переезжает суть реализованной задачи** — спеки, ADR, архив изменений:
|
2. **Что считается необратимым** и потому спрашивается у человека всегда
|
||||||
без этого не проверить, что задача закрыта не коммитом, а решением.
|
|
||||||
4. **Что считается необратимым** и потому спрашивается у человека всегда
|
|
||||||
(деплой, выкладка наружу, удаление или перезапись данных).
|
(деплой, выкладка наружу, удаление или перезапись данных).
|
||||||
5. **Оракулы, которые в проекте вообще есть** — чем проверяется критерий
|
|
||||||
приёмки: тест, команда, прогон на реальных данных, глазами по логу.
|
|
||||||
6. **Команда учёта задач** — готовая строка, которой чужой контекст зовёт
|
|
||||||
`tasks.py`, потому что путь к плагину ему неизвестен. Например:
|
|
||||||
|
|
||||||
```
|
Ни того ни другого скилл не угадывает: не нашёл — спрашивает пользователя, а не
|
||||||
Команда учёта задач: python3 ~/.claude/plugins/marketplaces/av-dev-skills/\
|
|
||||||
av-dev-tasks/skills/tasks/scripts/tasks.py --dir docs/tasks
|
|
||||||
```
|
|
||||||
|
|
||||||
Слот заполняется один раз при подключении плагина. Он же отвечает на вопрос
|
|
||||||
«кто закрывает задачу»: команду знает проект, зовёт её владелец спринта.
|
|
||||||
|
|
||||||
Ничего из этого скилл не угадывает: не нашёл — спрашивает пользователя, а не
|
|
||||||
подставляет умолчание.
|
подставляет умолчание.
|
||||||
|
|
||||||
## Общее для всех сценариев
|
## Общее для всех сценариев
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
# Адаптация каталога задач
|
||||||
name: adopt
|
|
||||||
description: Прийти в чужой репозиторий и вывести каталог задач из того, что там уже есть — старая раскладка беклога (README-индекс, CLOSED-кладбище, транслитные слаги), TODO.md, россыпь заметок, раздел «планы» в README, список шагов в плане проекта. Сперва карта находок и целей человеку, запись только после подтверждения; слаги переименовываются в английские вместе с починкой перекрёстных ссылок. Использовать, когда просят перевести проект на этот формат задач, перенести беклог, адаптировать существующие заметки под цели и спринты. Разовая операция: дальше проект ведут скиллы tasks и session.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Адаптация чужого репозитория
|
Проект, где задачи уже как-то ведутся, и из имеющегося материала **выводится**
|
||||||
|
заполненный каталог задач: цели, задачи, кладбище, индексы. Операция разовая —
|
||||||
|
после неё проект живёт скиллами `tasks` и `session`.
|
||||||
|
|
||||||
Плагин приходит в проект, где задачи уже как-то ведутся, и **выводит** из
|
**Это часть приведения проекта к канону.** Раскладку `docs/` целиком ведёт скилл
|
||||||
имеющегося материала заполненный каталог задач: цели, задачи, кладбище, индексы.
|
`av-dev-pm:canon`; он же зовёт этот сценарий на шаге «каталог задач», потому что
|
||||||
Операция разовая — после неё проект живёт скиллами `tasks` и `session`.
|
форматом задач владеет `tasks`, а не `canon`. Отдельно сценарий вызывается,
|
||||||
|
когда переводить надо **только** задачи.
|
||||||
|
|
||||||
Вход какой угодно: старая раскладка `av-dev-backlog` (индекс `README.md`,
|
Вход какой угодно: старая раскладка `av-dev-backlog` (индекс `README.md`,
|
||||||
кладбище `CLOSED.md`, приоритеты секциями, транслитные слаги, файлы рядом с
|
кладбище `CLOSED.md`, приоритеты секциями, транслитные слаги, файлы рядом с
|
||||||
@@ -61,10 +61,10 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
|||||||
|
|
||||||
## Порядок
|
## Порядок
|
||||||
|
|
||||||
1. **Осмотрись.** Где лежат задачи, план, заметки; читается ли `CLAUDE.md`
|
1. **Осмотрись.** Где лежат задачи, план, заметки. Каталог задач по канону —
|
||||||
проекта — там может быть указатель на каталог. Секции беклога проекта
|
всегда `docs/tasks`. Секции беклога (`--sections`) — по умолчанию
|
||||||
(`--sections`) — по умолчанию `ядро,инфра`; если у проекта деление другое по
|
`ядро,инфра`; если у проекта деление другое по существу, оно называется
|
||||||
существу, оно называется здесь, а не подгоняется под умолчание.
|
здесь, а не подгоняется под умолчание, и уезжает в `docs/.pm.json`.
|
||||||
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
|
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
|
||||||
прохода дадут два несогласованных состояния.
|
прохода дадут два несогласованных состояния.
|
||||||
3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи;
|
3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи;
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "av-dev-tasks",
|
|
||||||
"description": "Управление задачами как каталогом markdown-файлов: цели вместо приоритетов, спринт под одну цель с заморозкой набора, вопросы и блокеры, каденция «разбор — переоценка — набор». Заведение задач из диалога и из находок ревью, декомпозиция, штурм идей, разовая адаптация чужого репозитория под этот формат. Не выполняет задачи — этим занимается пайплайн проекта.",
|
|
||||||
"author": {
|
|
||||||
"name": "Anton Vakhrushev",
|
|
||||||
"email": "anwinged@gmail.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user