Compare commits
9
Commits
8ce2a29160
...
cbfae90f3f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbfae90f3f
|
||
|
|
47a2f3de63
|
||
|
|
2d39a77444
|
||
|
|
c3e0a6d01f
|
||
|
|
52cc4d05d4
|
||
|
|
354a6b03d5
|
||
|
|
228b6c7eee
|
||
|
|
069205ac69
|
||
|
|
d7e9740c73
|
@@ -19,11 +19,6 @@
|
||||
"name": "av-dev-git",
|
||||
"source": "./av-dev-git",
|
||||
"description": "Git-обвязка для личных проектов. Скилл commit — сообщения коммитов в личном стиле (русский, scope-префикс, средняя детализация, без co-authored)."
|
||||
},
|
||||
{
|
||||
"name": "av-dev-backlog",
|
||||
"source": "./av-dev-backlog",
|
||||
"description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями. Оставлен до перевода последнего проекта; новые проекты не подключают."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
__pycache__/
|
||||
.venv/
|
||||
.ruff_cache/
|
||||
tmp/
|
||||
|
||||
/NOTES.md
|
||||
|
||||
+537
-7
@@ -261,7 +261,7 @@ jellybit — секреты в `conventions/config.md`. **Периметра н
|
||||
проверять сознательно` требует ссылки на его запись.
|
||||
|
||||
**L. Журнал расширяется до всех воспроизведённых дефектов** с пометкой
|
||||
«проскочил / пойман ревью». Эвал-сет для калибровки — выборка по пометке.
|
||||
«проскочил / пойман ревью». Проверочный набор для калибровки — выборка по пометке.
|
||||
|
||||
*Причина:* пойманные дефекты с оракулом (768 МиБ пика, 5.019 с удержания
|
||||
блокировки) сегодня не сохраняются нигде, кроме отчётов триажа в архиве change, а
|
||||
@@ -373,8 +373,9 @@ openspec/
|
||||
докладывает исход, записей учёта не трогает.
|
||||
|
||||
**Q. `av-dev-backlog` помечается устаревшим и остаётся** до перевода jellybit.
|
||||
Описание переписывается так, чтобы не ловить триггер «добавь задачу в беклог» —
|
||||
иначе агент выбирает между ним и `av-dev-pm` случайно.
|
||||
*(заменено на тему 30: плагин удалён раньше этого срока — условие пережило свою
|
||||
причину.)* Описание переписывается так, чтобы не ловить триггер «добавь задачу в
|
||||
беклог» — иначе агент выбирает между ним и `av-dev-pm` случайно.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
@@ -506,8 +507,8 @@ check`). Плюс `openspec/specs/` вливает `opsx:archive`.
|
||||
|
||||
*Причина:* это тот же приём, что «границы покрытия» в отчёте ревью и «пустой
|
||||
пункт называется пустым» в каноне — и единственный, о котором в этом репозитории
|
||||
есть данные, что он работает. Умолчание «не написал» становится неотличимым от
|
||||
«написал, что не требуется», только если отрицание обязательно.
|
||||
есть данные, что он работает. Отличить «не написал» от «написал, что не
|
||||
требуется» можно только тогда, когда отрицание обязательно.
|
||||
|
||||
**Триггер ADR, окончательная формулировка.** Запись заводится, когда верно одно
|
||||
из трёх: **дорогой откат** (переделка стоит дороже переписывания одного файла);
|
||||
@@ -730,7 +731,9 @@ pyrefly: в окружении нет ничего, кроме линтеров,
|
||||
опечатка, и три этих правила давали **311 срабатываний из 338** — шум, в котором
|
||||
тонут остальные 27.
|
||||
|
||||
**HH. `av-dev-backlog` исключён из проверки.** Плагин помечен устаревшим и живёт
|
||||
**HH. `av-dev-backlog` исключён из проверки.** *(исчерпано темой 30: плагин
|
||||
удалён, исключение снято из `pyproject.toml` и `copies.py`.)* Плагин помечен
|
||||
устаревшим и живёт
|
||||
до перевода последнего проекта, после чего удаляется целиком. Шесть его находок
|
||||
косметические (`os.replace`, `l` как имя), а правка замороженного кода без
|
||||
тестов — риск без выгоды. Исключение уходит вместе с плагином.
|
||||
@@ -1028,7 +1031,7 @@ HTML-комментарии, невидимые в отрендеренном ma
|
||||
|
||||
**AAA. Ребро — это порядок, никогда не данные.** В обычном графе задач ребро
|
||||
тянет за собой вывод предшественника; здесь это запрещено: проход, увидевший
|
||||
чужие находки, соглашается с ними, и декорреляция — вся ценность конвейера —
|
||||
чужие находки, соглашается с ними, и разведённость — вся ценность конвейера —
|
||||
обнуляется. Сказано в самом правиле, потому что графовый словарь провоцирует
|
||||
ровно эту ошибку. Исключение одно и оно же сток: триаж.
|
||||
|
||||
@@ -1728,3 +1731,530 @@ ADR, запискам разведки и сообщениям коммитов
|
||||
Из пяти языковых находок три приняты сразу, две отклонены — и обе отклонённые
|
||||
указывали на одно и то же место канона (правило 4 без оговорки). Вкусовых
|
||||
находок не было ни одной: порог держится.
|
||||
|
||||
## 25. Секция `Сопровождение` и общий словарь трёх мест (2026-08-04)
|
||||
|
||||
### Что было
|
||||
|
||||
`Разработка` — имя, которое называло слишком много: роадмап **весь** про
|
||||
разработку, и секция с таким именем не отличалась от остальных ничем. Предложено
|
||||
`Сопровождение` (англ. `Operations`).
|
||||
|
||||
### Решено
|
||||
|
||||
**ЧЧЧ. Секция называется `Сопровождение` / `Operations`, и её смысл расширен.**
|
||||
Было «инструмент и процесс», стало «чем держат проект: инструмент, процесс,
|
||||
эксплуатация». Расширение не косметическое: английское `Operations` при узком
|
||||
смысле обещало бы эксплуатацию, а внутри лежал бы линтер. Либо слово, либо
|
||||
смысл — сошлись на смысле, потому что метрики, логи, инфраструктура и выкладка
|
||||
в эту секцию просятся и так.
|
||||
|
||||
**ШШШ. Версия канона не менялась, и это законно.** ~~Ни один проект на каноне 3
|
||||
не стоит: healthlog и jellybit держат канон 2, повышение только предстоит.~~
|
||||
|
||||
**Отменено в тот же день (тема 26).** Посылка была ложной: healthlog уже переехал
|
||||
на канон 3, и правка записи версии 3 задним числом переписывала то, по чему он
|
||||
ехал. Правило осталось верным, применение — нет: черновиком запись версии
|
||||
является ровно до того, как **первый** проект по ней поехал.
|
||||
|
||||
**ЩЩЩ. Сопровождение и эксплуатация — целое и часть, и словарь у трёх мест
|
||||
общий.** Тема живёт в трёх документах, и раньше каждое место говорило своим
|
||||
словом. Теперь: сопровождение — всё, чем держат проект (инструмент, процесс,
|
||||
выкладка, метрики, логи, инфраструктура, дежурство); эксплуатация — его часть,
|
||||
работа системы на проде.
|
||||
|
||||
| Место | Уровень | Что там |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md`, секция `Сопровождение` | план | работы, которые собираемся делать |
|
||||
| `architecture.md`, раздел «Эксплуатация» | состояние | как устроено сейчас |
|
||||
| эксплуатационный проход ревью | оптика | «это упало через неделю на проде» |
|
||||
|
||||
**Сливать три места в одно слово было бы ошибкой**: они отвечают на разные
|
||||
вопросы — план, состояние, проверка. Синхронизирован **словарь**, а не границы;
|
||||
дом словаря — `canon.md`.
|
||||
|
||||
Слово **«поддержка» запрещено вовсе**: в нём слышится помощь пользователю, а это
|
||||
третья работа, к этим двум не относящаяся.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
96. **Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
|
||||
сообщает о своём состоянии» — возможность (наблюдает пользователь сервиса);
|
||||
«дежурный видит состояние на одном экране» — сопровождение (наблюдаем мы).
|
||||
Одни и те же метрики попадают в разные секции роадмапа, и это верно.
|
||||
97. **`check --fix` чужую секцию не переименовывает — и правильно.** На
|
||||
переименовании `Разработка` → `Сопровождение` проверка назвала секцию
|
||||
роадмапа чужой и остановилась: регистр она правит сама, смысл — нет. Ровно
|
||||
то поведение, которое нужно проекту при повышении канона.
|
||||
|
||||
## 26. Канон 4: правка задним числом отменена (2026-08-04)
|
||||
|
||||
### Что было
|
||||
|
||||
Секцию `Разработка` переименовали в `Сопровождение` без повышения версии канона —
|
||||
на посылке «ни один проект на каноне 3 не стоит» (тема 25, ШШШ). Посылка
|
||||
оказалась ложной: healthlog уже переехал, `docs/.pm.json` держит `"canon": 3`, а
|
||||
роадмап — секцию `Разработка` с прописной. Правка записи версии 3 переписывала
|
||||
то, по чему он ехал.
|
||||
|
||||
### Решено
|
||||
|
||||
**ЭЭЭ. Запись версии — черновик ровно до первого переехавшего проекта.** После
|
||||
этого она **история**, и любое изменение канона заводит новую версию, даже если
|
||||
меняется одно слово. Проверять это дёшево: `grep '"canon"' */docs/.pm.json` по
|
||||
живым проектам. Дорого — обратное: проект, повышенный по тексту, которого больше
|
||||
не существует, невоспроизводим.
|
||||
|
||||
Запись версии 3 восстановлена дословно (`Разработка` | `Tooling`), переименование
|
||||
уехало в версию 4. jellybit, стоящий на каноне 2, прочтёт обе записи подряд и
|
||||
заведёт `Разработка`, чтобы через шаг переименовать; в шаг версии 3 добавлена
|
||||
оговорка «едешь сразу на 4 — заводи `Готово` последней и не переставляй дважды».
|
||||
Лишний шаг — плата за честную историю, и она мала.
|
||||
|
||||
**ЮЮЮ. `Готово` переехало вниз, и порядок секций стал каноническим.**
|
||||
Достигнутое **копится**: через год этой секции больше, чем всех остальных
|
||||
вместе, — и стоя первой она отодвигает за экран ровно то, ради чего роадмап
|
||||
открывают чаще всего. Порядок теперь проверяется (`roadmap_lint`) и правится
|
||||
(`check --fix` переставляет секции вместе с содержимым): без проверки порядок
|
||||
разъедется молча, а переставлять секцию с десятком строк руками — работа, на
|
||||
которой ошибаются.
|
||||
|
||||
**ЯЯЯ. Индексы позиций считаются из самого кортежа.** `ACHIEVED` был `0` и стал
|
||||
`3`; хардкод индексов пережил бы перестановку молча и сломал бы `close`. Теперь
|
||||
`PLANNED, DIRECTIONS, OPERATIONS, ACHIEVED = range(len(ROADMAP_SECTIONS))` —
|
||||
переставили секцию, индексы переехали сами.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
98. **Отбивка нужна и перед заголовком.** Перестановка блоков ставит два
|
||||
заголовка вплотную — `spaced_sections` правил только строку после. Дефект
|
||||
нашёлся сразу же, на первой перестановке демо-набора: класс правки,
|
||||
существующий только потому, что появилась другая правка.
|
||||
99. **`check --fix` переставляет, но не переименовывает.** Чужую секцию он
|
||||
оставляет ошибкой, и на переименовании `Разработка` → `Сопровождение`
|
||||
останавливается: имя — решение человека, порядок — механика. Тот же разрез,
|
||||
что между регистром (правит) и составом (не трогает).
|
||||
100. **Версия канона отделяет состояния проектов, а не редакции текста** — и
|
||||
ровно поэтому её нельзя не поднять, когда состояние хоть одного проекта
|
||||
уже зафиксировано.
|
||||
|
||||
## 27. Тип записи стал единственной осью и задаёт схему (2026-08-05)
|
||||
|
||||
Заметка просила «каждый тип задач сделать своей сущностью»: эмодзи на тип, тип
|
||||
первым полем меты, категория вместо секции, описание типа с обязательными
|
||||
разделами и алгоритмом, идеи в конец. Разбор показал, что первый шаг обязан быть
|
||||
другим — не добавить типу свойств, а **сократить число осей**.
|
||||
|
||||
**ААББ. Осей было две, и ортогональность была фальшивой.** Тип записи
|
||||
(`goal`/`idea`/`task`) и род работы (`kind:<род>` тегом) давали двенадцать клеток
|
||||
произведения, из которых законны шесть: у цели род запрещён, у задачи обязателен,
|
||||
у идеи пуст и на практике не ставится. Плюс «алгоритм работы над записью такого
|
||||
типа» крепится не к `task`, а к `fix` и `research` — то есть к роду. Ось, к
|
||||
которой пишется алгоритм, и была настоящим типом. Оси схлопнуты в одну из пяти
|
||||
значений: `goal` | `feature` | `fix` | `chore` | `research`.
|
||||
|
||||
**ВВГГ. Тип `idea` упразднён: состояние не может быть типом.** Он значил не род
|
||||
работы, а незаполненность — «первый, второй или третий вопрос теста готовности не
|
||||
отвечается». Состояние меняется по мере того, как запись дописывают, а тип меняют
|
||||
командой, и на этом расхождении `idea` и жила: её приходилось «понижать» и
|
||||
«повышать» вручную. Теперь состояние выводится из заполненности — **`research`
|
||||
без раздела «Вопрос» это сырьё**, — и различие держит та же машина, что и всё
|
||||
остальное.
|
||||
|
||||
Цена решения названа сразу: `research` теперь вбирает и замер реальности, и
|
||||
сырую функцию («Подсказка следующего хода»). Обосновано это тем, что у обоих
|
||||
**один исход — записанный ответ, а не изменение системы**, и одна приёмка. Имя
|
||||
`rnd` из заметки отклонено в пользу `research`: аббревиатура читается как
|
||||
`random` и не расшифровывается тому, кто вернётся к беклогу через квартал, а
|
||||
`research` уже стоял в файлах живых проектов — миграция тронула только бывшие
|
||||
идеи.
|
||||
|
||||
**ДДЕЕ. Дом типа — поле меты, эмодзи производна.** Прежнее правило «отдельного
|
||||
поля типа нет: два места для одного факта разъезжаются» отменено не потому, что
|
||||
разонравилось, а потому, что его аргумент был против **префикса плюс поля**. При
|
||||
переносе дома в мету дом остаётся один; из индекса тип при этом пропадал бы — там,
|
||||
где принимают решение «брать или не брать», — и это чинит эмодзи. Она стоит в H1,
|
||||
а не в строке индекса, чтобы инвариант «заголовок в индексе дословно» остался
|
||||
нетронутым: одна проверка вместо двух.
|
||||
|
||||
**ЖЖЗЗ. Поле места назвали по типу, а не одним словом на всех.** «Категория»
|
||||
вместо «Секции» — просьба заметки, но одинаковое переименование закрепило бы
|
||||
смешение: у задачи поле называет полку домена, в которую она вернётся из
|
||||
спринта, у цели — часть роадмапа, то есть состояние очереди. Разные имена
|
||||
(`Категория` / `Секция`) выбраны именно потому, что **какое поле обязательно,
|
||||
решает тип** — то самое, ради чего затевалась вся правка.
|
||||
|
||||
**ИИКК. Два новых обязательных раздела появились из уже записанных правил,
|
||||
которые нечем было проверить.** «Не воспроизводится — это `research`, а не `fix`»
|
||||
стояло в каноне и не проверялось: раздел `Воспроизведение` делает его
|
||||
проверяемым. Приёмка разведки — «записанный ответ, а не изменённый код» — тоже
|
||||
стояла, но `sprint take` требовал от `research` два-пять критериев с оракулами,
|
||||
и они писались ради проверки; вместо них `Вопрос` и `Куда ляжет ответ`.
|
||||
|
||||
**ЛЛММ. Сортировка «по важности» отклонена, «сырьё в конец» взято.** Первая
|
||||
требует, чтобы кто-то важность поддерживал, — это ровно тот приоритет, от
|
||||
которого правило 4 отказалось сознательно. Вторая **выводится из типа и
|
||||
заполненности**, а не назначается человеком, и потому проверяется машиной и
|
||||
приоритетом не становится. Разрез прошёл по признаку «кто источник порядка», а не
|
||||
по признаку «полезно ли».
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
101. **Правило можно отменять его собственным аргументом.** «Отдельного поля типа
|
||||
нет» держалось на «два места для одного факта»; перенос дома оставил одно
|
||||
место, и правило перестало применяться. Проверять надо не запись правила, а
|
||||
то, выполняется ли ещё его посылка.
|
||||
102. **Схема, шаблон и проверка растут из одной таблицы.** `TYPE_SCHEMA` кормит и
|
||||
`body_template`, и `schema_verdict`: иначе `add` кладёт то, на чём
|
||||
`sprint take` потом откажет. Тот же приём, что нормализатор `spaced_sections`
|
||||
для оформления индексов.
|
||||
103. **`--fix` не угадывает того, чего нет.** Тип переносится из тега `kind:` и
|
||||
префикса `[goal]`/`[idea]` детерминированно, но записи, заведённые до
|
||||
появления рода работы, не несут ни того ни другого — `feature` от `chore`
|
||||
машина не отличает. Они уходят в `НЕОДНОЗНАЧНО` поимённо, а не получают
|
||||
значение по умолчанию, которое врало бы ровно там, где по нему принимают
|
||||
решение.
|
||||
104. **Мигрирующие шаги обязаны читать отложенный текст, а не диск.** Шагов,
|
||||
правящих мету, стало пять, и второй, перечитавший файл, стёр бы правку
|
||||
первого. Общий `stage()` поверх `files` снял целый класс отказов, который до
|
||||
этого держался на том, что шагов было мало.
|
||||
|
||||
## 28. Слаг подкреплён проверкой, обещанный судья заведён (2026-08-05)
|
||||
|
||||
Два пункта заметок, оба про одно: правило было записано и никем не исполнялось.
|
||||
|
||||
**ННОО. Правило про английские слаги существовало и не проверялось ничем.**
|
||||
`canon.md` говорил «слаги файлов, capability и задач — английские, kebab-case»
|
||||
одной строкой в хвосте раскладки; `docs.py` имён файлов не смотрел вовсе. Итог
|
||||
предсказуем и нашёлся в самом плагине: единственный пример ADR в скилле `docs`
|
||||
назывался `ADR-2026-08-03-ochered-tablicej`. Раскладка канона при этом
|
||||
приглашала к нарушению — в схеме стояли плейсхолдеры `<тема>.md`, то есть слово
|
||||
«тема» по-русски там, где надо было писать `<slug>`.
|
||||
|
||||
Разрез проверки — по тому, что машина знает точно: кириллица в имени и не-kebab-case
|
||||
**жёстко**, форма `ADR-ГГГГ-ММ-ДД-slug.md` жёстко, транслит **эвристикой**, то есть
|
||||
замечанием. Набор маркеров транслита подобран так, чтобы **ложных срабатываний не
|
||||
было вовсе**: выброшены `ost` (ловит `post`, `cost`), `sch` (`schema`), `ya`
|
||||
(`yaml`), `nost` (`nostalgia`), хвост `ii` (`radii`). Цена названа: `sostoyanie-partii`
|
||||
проходит мимо. Правило, краснеющее на правде, приучает пролистывать весь блок —
|
||||
это дороже пропуска.
|
||||
|
||||
**ППРР. Канон три версии обещал судью, которого не было.** В `canon.md` есть
|
||||
таблица «Что проверяет машина, а что человек», и её правая колонка — смысловой
|
||||
дубль, поведение в `architecture.md`, протухший факт, достаточность честной
|
||||
строки — описывала работу, которую никто не делал: скилл `canon` предлагал
|
||||
агенту судить об этом самому, то есть проверять то, что он же и писал. Заведены
|
||||
`doc-consistency` и `doc-code-drift`, а колонка получила третий столбец с именем
|
||||
судьи: обещание без адресата и есть тот способ, которым правило перестаёт
|
||||
исполняться.
|
||||
|
||||
**ССТТ. Агентов двое, разрез по глубине, а не по охвату.** Тот же довод, что
|
||||
развёл `task-form` и `doc-wording`: сверка текста с текстом дёшева и зовётся на
|
||||
каждом синке документации, сверка с кодом требует читать репозиторий и зовётся
|
||||
раз в спринт. Слитый агент делает дешёвую половину редкой либо дорогую —
|
||||
поверхностной.
|
||||
|
||||
**УУФФ. Перечень фактов, сверяемых с кодом, закрыт.** Имя основной ветки,
|
||||
команды, пути, зависимости поимённо, настройки с числовым значением, единые точки
|
||||
проекта, capability, проверяемые инварианты. «Сверить архитектуру с кодом» —
|
||||
задача без дна, и агент, которому её поставили, выдаёт правдоподобную труху
|
||||
вместо находок. Отсюда и форма доклада `doc-code-drift`: он начинается **таблицей
|
||||
проверенного**, а не находками, — по ней видно, чего он не смотрел.
|
||||
|
||||
**ХХЦЦ. Карта домов уехала в устав агента помеченной копией.** Устав ссылался на
|
||||
файл плагина, а агент работает в репозитории проекта, где плагина может не быть.
|
||||
Копия дословная, под маркерами `дом`/`копия`, и `copies.py` теперь её сторожит —
|
||||
механизм для этого в репозитории уже был.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
105. **Записанное правило без проверки не исполняется даже автором.** Слаг ADR
|
||||
нарушен в единственном примере, который плагин показывает как образец. Тот
|
||||
же класс, что «прозаический триггер ADR дал 6 записей на 43 изменения»:
|
||||
умолчание становится отличимым только когда его проверяют.
|
||||
106. **Плейсхолдер — часть правила.** `<тема>.md` в схеме раскладки перевешивал
|
||||
строку правила, стоявшую двумя абзацами ниже: образец читают вместо текста.
|
||||
107. **Эвристика настраивается по ложным срабатываниям, а не по полноте.** Ноль
|
||||
ложных при одном пропуске лучше, чем наоборот: пропуск стоит одной ненайденной
|
||||
находки, ложное срабатывание — доверия ко всему блоку.
|
||||
108. **Докстрока разошлась с кодом ровно там, где её читают.** `copies.py`
|
||||
показывал закрывающие маркеры как `<!-- /дом -->`, а требовал
|
||||
`<!-- /дом: <id> -->`; нашлось это первой же попыткой ими воспользоваться.
|
||||
Пример в докстроке — тот же образец, что плейсхолдер в схеме.
|
||||
|
||||
## 29. Обкатка `doc-consistency` на самом dev-skills (2026-08-05)
|
||||
|
||||
Первый прогон агента — по репозиторию, который его же и содержит. Два прохода
|
||||
(av-dev-pm; пайплайн плюс верхний уровень), 17 находок, все подтверждены по
|
||||
файлам.
|
||||
|
||||
**ЧЧШШ. Агент нашёл ровно тот класс, ради которого заводился, и в свежей
|
||||
работе.** Пять находок — остатки прежней модели типов в файлах, которые я не
|
||||
дошёл поправить двумя коммитами раньше: `adopt.md` держал имена секций **канона
|
||||
2**, `from-review.md` и `TODO.md` — упразднённый `[idea]`, `task-batch` в другом
|
||||
плагине — «задачи-идеи». Правку модели я вёл от документов, которые менял, а не
|
||||
от документов, которые на них ссылаются, — и обратный обход не сделал ни разу.
|
||||
|
||||
**ЩЩЪЪ. Самая дорогая находка была моей и свежей.** Таблица типов в `canon.md`
|
||||
объявляла цель у `fix` запрещённой, а `tasks/SKILL.md` и `task-fix.md` —
|
||||
необязательной; код на стороне вторых. Копия разошлась с домом **за один
|
||||
день** — я написал обе половины в одном коммите. Это и есть цена второго дома в
|
||||
чистом виде: не «когда-нибудь разойдётся», а «разошлось прежде, чем высохли
|
||||
чернила».
|
||||
|
||||
Исход не «поправить значение», а **убрать причину**: `canon.md` дважды объявлял,
|
||||
что фиксирует только словарь типов, — значит колонкам «разделы» и «цель» в нём
|
||||
не место. Осталась таблица из двух колонок и ссылка на дом схемы.
|
||||
|
||||
**ЫЫЬЬ. Копии перечня «чем держат проект» разъехались втроём.** `canon.md`,
|
||||
`tasks/SKILL.md` и `task-goal.md` пересказывали его своими словами: «метрики и
|
||||
логи» против «мониторинга», «проверки» есть в двух из трёх. При этом
|
||||
`tasks/SKILL.md` **ссылался на дом рядом с собственным пересказом** — ссылка не
|
||||
мешает копии разойтись, если копия всё равно стоит.
|
||||
|
||||
**ЭЭЮЮ. Находка про коммиты снята как неверная, и это дефект самого агента.**
|
||||
Он прочитал `av-dev-git/skills/commit/SKILL.md` («без `Co-Authored-By`») как
|
||||
описание практики этого репозитория и предъявил 38 коммитов с трейлером. Но
|
||||
dev-skills — **маркетплейс плагинов**: скилл коммита здесь продукт, уезжающий в
|
||||
чужие проекты, а не правило, которому подчиняется сам репозиторий. Устав агента
|
||||
не различает «документ описывает этот репозиторий» и «документ описывает то, что
|
||||
репозиторий производит».
|
||||
|
||||
**ЮЮЯЯ. Счётчики в документах отменены как класс.** `REMAINING.md` держал «после
|
||||
разбора двенадцати тем и 16 коммитов» (стало 28 и 52) и «три неизмеренных
|
||||
изменения подряд» (стало больше). Оба числа обязан двигать человек, и оба
|
||||
отстали молча. Заменены на формулировки, которые не надо поддерживать, и в шапку
|
||||
записана причина.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
109. **Правка модели идёт по обратным ссылкам, а не по изменённым файлам.**
|
||||
Меняешь дом — обойди тех, кто на него ссылается: `grep` по упразднённому
|
||||
слову дал бы все пять остатков за минуту. Это дешевле любого агента и
|
||||
должно идти до него.
|
||||
110. **Ссылка на дом не отменяет копию, стоящую рядом.** Проверять надо не
|
||||
«есть ли ссылка», а «есть ли пересказ»; `tasks/SKILL.md` имел и то и другое.
|
||||
111. **Копия расходится с домом в пределах одного коммита.** Прежняя оценка
|
||||
(«разойдётся на первой правке») занижена: расхождение возникает при
|
||||
написании, если оба места пишет один проход.
|
||||
112. **Агент, читающий репозиторий-продукт, обязан различать «про нас» и «про
|
||||
то, что мы производим».** Иначе он предъявляет продукту практику его
|
||||
потребителя. Устав `doc-consistency` этого различения не содержит — остаток
|
||||
записан в REMAINING.
|
||||
113. **Число в документе — обязанность, которую никто не берёт.** Счётчик тем,
|
||||
коммитов, правок протухает молча; формулировка без числа дешевле его
|
||||
сопровождения.
|
||||
|
||||
## 30. `av-dev-backlog` удалён (2026-08-05)
|
||||
|
||||
Плагин был помечен устаревшим решением Q и жил до перевода jellybit. Удалён
|
||||
раньше этого срока.
|
||||
|
||||
**ААББВВ. Замороженный плагин стоит дороже, чем кажется.** Он не менялся, но
|
||||
платил собой в каждой проверке репозитория: `exclude` в `pyproject.toml`,
|
||||
`SKIP_DIRS` в `copies.py`, два абзаца README, оговорка в описании маркетплейса,
|
||||
чтобы не ловить триггер «добавь задачу в беклог». Пять исключений ради кода,
|
||||
который никто не читает, — и каждое надо было объяснять всякий раз, когда
|
||||
кто-нибудь спрашивал, почему проверка обходит каталог.
|
||||
|
||||
**ААББГГ. Понимание старой раскладки уехало из плагина раньше самого плагина.**
|
||||
`docs/backlog/` читает не `backlog.py`, а `av-dev-pm:tasks` — `adopt.md` и
|
||||
адаптер в `tasks.py` держат ту же раскладку как **вход миграции**. Плагин
|
||||
перестал быть единственным, кто её знает, ещё когда писался `adopt`; условие
|
||||
«живёт до перевода последнего проекта» с тех пор охраняло пустоту.
|
||||
|
||||
**ААББДД. Опасение про порядок снятия не подтвердилось.** Удаление опередило
|
||||
снятие: на jellybit плагин оставался включённым, когда записи в маркетплейсе уже
|
||||
не было, и ожидалась ручная чистка `enabledPlugins` и `installed_plugins.json`.
|
||||
`claude plugin uninstall` отработал штатно — он идёт **по реестру, а не по
|
||||
манифесту маркетплейса**, и отсутствие записи там ему безразлично. Предупреждение
|
||||
из README снято, вместо него записан проверенный факт.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
114. **Устаревшее удаляют, а не замораживают.** Заморозка выглядит бесплатной,
|
||||
но растекается исключениями по конфигам и требует объяснения в каждом
|
||||
месте, куда попала. Если удалять пока рано — назвать условие и срок; условие
|
||||
без срока переживает свою причину.
|
||||
115. **Условие «живёт до X» проверяют на живость, а не на X.** Здесь X (перевод
|
||||
jellybit) не наступил, но причина условия отпала раньше: знание раскладки
|
||||
переехало в `adopt`. Перепроверять надо основание, иначе условие держит само
|
||||
себя.
|
||||
116. **Порядок снятия и удаления из маркетплейса свободный.** `uninstall` живёт
|
||||
реестром, манифест ему не нужен. Правило записано после проверки, а не
|
||||
из осторожности, — и осторожность здесь стоила бы лишнего абзаца в README
|
||||
про починку, которой не бывает.
|
||||
|
||||
## 31. Ревизия покрытия `av-dev-pm` продакт-оптикой (2026-08-05)
|
||||
|
||||
Сабагент в роли продакт-менеджера оценил покрытие жизненного цикла личного
|
||||
проекта (один человек, недели-месяцы) скиллами и агентами `av-dev-pm`. Скоуп
|
||||
сужен по ходу разбора: деплой и разбор инцидентов на проде делаются вручную,
|
||||
скиллов под них не заводим. Осталось планирование, разработка и доработка.
|
||||
|
||||
**ААББЕЕ. Шаг 2 сессии требовал чисел, которых процесс отказался собирать
|
||||
решением.** `cadence.md` делал обязанностью пересмотр «ориентира по размеру
|
||||
спринта, прироста беклога на закрытую задачу, времени на задачу» и «сколько
|
||||
заняли задачи **против ожидания**» — с обоснованием «иначе обязанность висит
|
||||
ничья». Данных под это нет: у записи нет дат заведения, взятия и закрытия,
|
||||
`close --implemented` удаляет файл, `sprint close` очищает `SPRINT.md`. Хуже
|
||||
того, «против ожидания» и «время на задачу» требуют оценки и тайм-бокса, а
|
||||
`session/SKILL.md` в «Почему не Scrum» их прямо не берёт: пункт противоречил
|
||||
решению, стоящему через файл от него.
|
||||
|
||||
Исход — **выкинуть, а не подпереть данными**. На практике числа не
|
||||
пересматривались ни разу, и заводить под них учёт дат значило бы обслуживать
|
||||
обязанность, которой никто не брал. Осталось качественное: что сломалось в
|
||||
процессе, что оказалось дороже, чем выглядело при заведении, какие правила не
|
||||
сработали. Шаг 3 пункт 9 переименован из «переоценки по измеренному» в
|
||||
«переоценку по пройденному», судит человек по памяти о спринте. Рядом записано,
|
||||
что замеров нет **намеренно** — иначе следующий читатель заведёт их обратно как
|
||||
недостающие.
|
||||
|
||||
**ААББЖЖ. `doc-consistency` переехал с каждого синка на сессию, к
|
||||
`doc-code-drift`.** Агент на `opus` зовётся шагом 9 пайплайна, то есть на каждой
|
||||
задаче: 5–8 opus-проходов за спринт по документам, которые за спринт меняются на
|
||||
несколько абзацев. Обоснование в каноне («сверка текста с текстом дёшева») верно
|
||||
относительно второго агента, но не в абсолюте на одиночке.
|
||||
|
||||
Довод сильнее денег: **расхождение между двумя документами по определению
|
||||
требует двух документов**, а на большинстве задач синк правит один. И пачка,
|
||||
отбираемая работой, не видит того, чего работа не касалась, — а расхождение живёт
|
||||
ровно там: правка отменяет решение в одном документе, парный статус нужен в
|
||||
другом. Это был открытый вопрос `REMAINING` про охват ADR при пересмотре; переезд
|
||||
его закрыл. Цена — потеря привязки находки к задаче, которая её породила: по теме
|
||||
29 именно эта привязка дала пять самых точных находок. Принято сознательно.
|
||||
|
||||
**ААББЗЗ. Отмена цели получила порядок, но не флаг.** `close` запрещал закрыть
|
||||
цель с живыми задачами, а что делать с этими задачами, не говорил нигде: шаг 3
|
||||
сессии знал только «та ли цель», `task-goal.md` описывал одно достижение, а
|
||||
`session/SKILL.md` вдобавок утверждал «цель постоянна». Человек получал отказ с
|
||||
перечнем и никакой подсказки.
|
||||
|
||||
Порядок записан: сперва задачи поштучно (`close --reason` своей причиной либо
|
||||
`edit --goal` на другую цель), потом сама цель через `close --reason` в
|
||||
`REJECTED.md`, а не в `Готово` — отменённая цель не умеет ничего. Флаг
|
||||
`--cascade` отвергнут: отмена цели редка и дорога, и поштучный разбор здесь не
|
||||
церемония, а единственный момент, когда видно, что из задач переживёт цель.
|
||||
Каскад превратил бы его в один Enter. **Причина у каждой задачи своя**: «цель
|
||||
отменена» это пересказ команды, в `REJECTED.md` от него нет пользы через квартал.
|
||||
|
||||
Место процедуры — переоценка на сессии, а не отдельный заход: отмена цели **и
|
||||
есть** разбор всех её задач, а разбор задач — шаг 3.
|
||||
|
||||
**ААББИИ. У брошенного спринта появился второй законный исход, без порога.**
|
||||
`--dissolve` во всех текстах был привязан к блокеру, и скрипт отказывал словами
|
||||
«роспуск объясняется блокером». Вернувшийся к набору, который стоял месяц, не
|
||||
имел законного хода: двигать нельзя (заморозка), распускать не по чему. Теперь
|
||||
роспуск объясняется блокером **или тем, что набор протух**.
|
||||
|
||||
Порога в неделях сознательно нет — это тот же класс, что выкинутые числа шага 2:
|
||||
счётчик простоя пришлось бы вести руками, а решает всё равно человек. Признак не
|
||||
срок, а **что набор перестал быть твоим**: перечитываешь, зачем эти задачи вместе
|
||||
— он протух. Туда же добавлена точка входа «вернулся, а спринт открыт»: `check`,
|
||||
`SPRINT.md`, развилка продолжать/распустить. Середины у развилки нет намеренно —
|
||||
«доделаю пару штук и решу» это работа по набору, которого ты не понимаешь.
|
||||
|
||||
**ААББКК. Журнал канона прогоняется как есть, а проверка исхода поручена
|
||||
судьям.** Схлопнуть записи 3 и 4 в один переход «с 2 на 4» отвергнуто: журнал
|
||||
описывает не только *что сделать*, но и порядок, в котором это делалось, и слитая
|
||||
запись экономит один проход ценой невоспроизводимости остальных. Оба живых
|
||||
проекта пройдут 2→3→4 по записям.
|
||||
|
||||
Взамен появилась проверка исхода: **шагом 6 `adopt` и шагом 6 `upgrade` зовутся
|
||||
оба судьи документов**. Это прямой ответ на открытый вопрос REMAINING «как
|
||||
проверять, что канон не разошёлся с проектами после `upgrade`»: `check` сверяет
|
||||
**число** в `.pm.json` с версией скрипта и про существо записи не знает ничего.
|
||||
Проект несёт `"canon": 4` и может не иметь того, чего требовала любая из
|
||||
пройденных версий — записи применяются руками, а ручной проход по трём записям
|
||||
подряд ровно то место, где половина шага делается и забывается.
|
||||
|
||||
У `adopt` добавка другого рода: там судьи ловят не недоделанную миграцию, а
|
||||
последствия переноса — факт, растащенный по двум домам, поведение, осевшее в
|
||||
`architecture.md`, ADR, оторванный от своего `design.md`. Им передаётся
|
||||
объявленное переходное состояние из шага 5, иначе честная строка в незаполненном
|
||||
слоте вернётся находкой.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
117. **Обязанность без источника данных отменяют, а не механизируют.** Первый
|
||||
позыв — дать шагу данные (дописать даты, сводку спринта). Но обязанность,
|
||||
не исполнявшуюся ни разу, дешевле снять: механизация под неё производит
|
||||
учёт, который надо вести, ради разбора, который не делается.
|
||||
118. **Требование, противоречащее решению через файл от него, — не мелочь, а
|
||||
признак копии.** «Против ожидания» пережило решение «не берём оценки»,
|
||||
потому что стояло в другом документе. Обратный обход по решению «что мы не
|
||||
берём» нашёл бы это сразу — тот же приём, что и следствие 109.
|
||||
119. **Частота вызова агента выводится из того, что он ищет.** Судья
|
||||
расхождений **между** документами бессмысленен там, где документ один;
|
||||
значит его место не на задаче, а на наборе задач. Цена вызова подтвердила
|
||||
вывод, но не она его дала.
|
||||
120. **Запрет обязан называть выход.** `close` верно не давал осиротить задачи,
|
||||
но текст отказа перечислял препятствия и молчал о ходе. Проверка без
|
||||
названного следующего шага — половина работы: она защищает данные и бросает
|
||||
человека.
|
||||
121. **Признак вместо порога там, где счётчик пришлось бы вести руками.**
|
||||
«Набор перестал быть твоим» проверяется в момент вопроса и ничего не
|
||||
требует хранить; «прошло N недель» требует учёта, который никто не ведёт, и
|
||||
всё равно кончается решением человека.
|
||||
122. **Версионирование без единого переехавшего проекта — не журнал миграций, а
|
||||
история правок.** Довод за схлопывание был верен по факту и отвергнут по
|
||||
принципу: обкатка на живых проектах и проверяет, работает ли механизм.
|
||||
Схлопнуть значило бы не прогнать его ни разу и оставить вопрос открытым.
|
||||
123. **Проверка версии не есть проверка миграции.** Число в `.pm.json` двигает
|
||||
тот же проход, что делал шаги, — и двигает независимо от того, все ли
|
||||
сделаны. Механической проверки существа нет; там, где её нет, ставится
|
||||
судья, а не отметка.
|
||||
|
||||
## 32. Сквозной проход по словарю: пять слов сняты, девять закрыты списком (2026-08-05)
|
||||
|
||||
Проход упрощения (тема 31) уткнулся в один и тот же класс у всех пяти агентов:
|
||||
слово, живущее в трёх-шести файлах разом. Правка в одном месте развела бы
|
||||
словарь, правка во всех — уже не упрощение текста скилла. Каждый агент честно
|
||||
остановился и записал слово в свой отчёт, и одни и те же слова всплыли в разных
|
||||
отчётах. Разобрано отдельным проходом.
|
||||
|
||||
**ААББЛЛ. «Слово прижилось» не проверяется, поэтому заменено списком.** Оговорка
|
||||
в `language.md` звучала так: не переводится «термин, у которого нет точного
|
||||
русского эквивалента и который в команде уже прижился». Проверить это на глаз
|
||||
нельзя — прижившимся выглядит любое слово, встреченное трижды, и ровно так пять
|
||||
агентов подряд и рассудили. Оговорка заменена **закрытым списком из девяти
|
||||
терминов** с колонкой «что называет»: интейк, триаж, провенанс, дедуп, чек-лист,
|
||||
дифф, промпт, сущности OpenSpec, роды проходов ревью. Слово не из списка и не из
|
||||
таблицы имён вещей — находка, а не принятый стиль.
|
||||
|
||||
Список заведён домом `язык-словарь` в `language.md` и копией в уставе
|
||||
`doc-wording`. Копия обязательна: агент работает в репозитории проекта, где
|
||||
плагина может не быть, и без списка предъявил бы «интейк» как англицизм.
|
||||
|
||||
**ААББММ. Пять слов сняты, и все пятеро выглядели словарём, не будучи им.**
|
||||
`конфляция` → смешение (4 места), `декорреляция` → разведённость (6),
|
||||
`непоймание` → почему не поймали (9), `эвал-сет` → проверочный набор (4), `гайд`
|
||||
→ руководство (6). Латинизм или калька при живом русском слове в каждом случае.
|
||||
|
||||
Разбор `декорреляции` показателен: проект **уже владел** нужным словом — «агенты
|
||||
разведены по глубине», «разведены по охвату» — и держал рядом латинский синоним
|
||||
того же понятия. Это не англицизм, а второй дом для слова.
|
||||
|
||||
`непоймание` снято ещё и потому, что форма журнала дефектов, которую канон кладёт
|
||||
в проекты, спрашивает «Почему не поймали» — а проза рядом называла это
|
||||
«причиной непоймания». Скелет и проза о скелете говорили разными словами.
|
||||
|
||||
**ААББНН. Снятое записано вместе с оставленным, в одном списке.** Иначе снятое
|
||||
возвращается: слово уходит из текстов, но ничто не мешает следующему проходу
|
||||
завести его заново — оно ведь короткое и точное. Пять слов названы поимённо с
|
||||
заменой каждого.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
124. **Escape hatch без перечня — это разрешение, а не исключение.** «Термин,
|
||||
который прижился» освобождает от правила любое слово: проверка «прижился ли»
|
||||
возвращает «да» всякий раз, когда слово встретилось. Исключение из правила
|
||||
обязано быть списком, иначе оно съедает правило.
|
||||
125. **Слово, от которого агент отказался править, — материал для отдельного
|
||||
прохода, а не мусор отчёта.** Пять независимых агентов сошлись на одном
|
||||
наборе слов, ни разу друг друга не видя. Список «что не тронул» оказался
|
||||
полезнее списка правок именно этим.
|
||||
126. **Снятое слово называется вместе с заменой и остаётся записанным.** Убрать
|
||||
из текстов недостаточно: без записи «это снято и вот чем заменено» слово
|
||||
возвращается первым же, кто найдёт его удачным.
|
||||
|
||||
@@ -14,12 +14,16 @@
|
||||
документация;
|
||||
- `canon` — привести проект к канону документов: `check` / `adopt` /
|
||||
`upgrade`, плюс скрипт `docs.py`. Там же живёт язык проектных текстов —
|
||||
информационный стиль, англицизмы, жаргон;
|
||||
информационный стиль, англицизмы, жаргон. Смысловую часть, которой скрипт
|
||||
не видит, судят два агента: `doc-consistency` (документы между собой и с
|
||||
openspec) и `doc-code-drift` (документы против кода);
|
||||
- `docs` — содержимое канона по ходу разработки: ADR из архивного
|
||||
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
|
||||
архитектуры;
|
||||
- `tasks` — задачи и цели каталогом markdown-файлов; вычитывают их два
|
||||
отдельных прохода: `task-form` (форма записи) и `doc-wording` (язык);
|
||||
- `tasks` — задачи и цели каталогом markdown-файлов, у каждой записи тип
|
||||
(`goal`, `feature`, `fix`, `chore`, `research`), и тип задаёт её схему;
|
||||
вычитывают их два отдельных прохода: `task-form` (форма записи) и
|
||||
`doc-wording` (язык);
|
||||
- `session` — ритуал между спринтами и ведение спринта.
|
||||
- **av-dev-pipeline** — исполнение. **Требует OpenSpec.**
|
||||
- `task-pipeline` — задача через полный цикл SDD, от постановки до коммита;
|
||||
@@ -29,8 +33,6 @@
|
||||
архитектура, обязательный триаж. Девять агентов-проходов, четыре ступени
|
||||
стоимости: `quick`, `standard`, `wide`, `deep`.
|
||||
- **av-dev-git** — `commit`: сообщения в личном стиле.
|
||||
- **av-dev-backlog** — **устарел**, заменён `av-dev-pm`. Живёт до перевода
|
||||
последнего проекта; как снять с проекта — [Снятие](#снятие).
|
||||
|
||||
Соглашение об именах: имя **плагина** длинное с префиксом `av-dev-`, имена
|
||||
**скилов** внутри — короткие. Вызов выходит вида `/av-dev-<плагин>:<скилл>`.
|
||||
@@ -68,29 +70,17 @@ flowchart TB
|
||||
## Канон документов проекта
|
||||
|
||||
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
|
||||
проектов много, и рядом OpenSpec тоже держит строгую структуру. Определение —
|
||||
[av-dev-pm/skills/canon/references/canon.md](av-dev-pm/skills/canon/references/canon.md).
|
||||
проектов много, и рядом OpenSpec тоже держит строгую структуру. `CLAUDE.md` плюс
|
||||
`docs/` (паспорт, архитектура, база, безопасность, конвенции, разведка, ADR,
|
||||
ревью, задачи) и `openspec/` — **раскладка целиком, роли документов и правило
|
||||
единственного дома живут одним домом**:
|
||||
[canon.md](av-dev-pm/skills/canon/references/canon.md). Здесь она не
|
||||
пересказывается: копия перечня путей уже расходилась с домом, и как раз в
|
||||
обязательных — в ней не хватало путей, чьё отсутствие `docs.py check` считает
|
||||
нарушением.
|
||||
|
||||
```
|
||||
CLAUDE.md инварианты с severity, команды, семантика гейта
|
||||
docs/
|
||||
.pm.json версия канона и пути для проверок
|
||||
passport.md зачем и для кого; чем НЕ является
|
||||
architecture.md как сложено — обзор; окружение и эксплуатация
|
||||
database.md схема хранилища; настройки с числовым значением
|
||||
security.md периметр; недоверенный вход; что вне модели
|
||||
conventions/ как пишем код + что уже механизировано
|
||||
research/ что показала реальность; числа с провенансом
|
||||
adr/ почему — промоут поверх архивных design.md
|
||||
review.md настройка конвейера + журнал дефектов
|
||||
tasks/ роадмап (что умеет), беклог, спринт, отклонённое
|
||||
openspec/
|
||||
specs/<capability>/spec.md что система делает — нормативно
|
||||
changes/archive/ архив изменений с design.md
|
||||
```
|
||||
|
||||
**Отдельного файла-брифа для ревью нет.** Проходы читают эти документы напрямую;
|
||||
карта «что нужно проходу → где лежит» —
|
||||
**Отдельного файла-брифа для ревью нет.** Проходы читают документы канона
|
||||
напрямую; карта «что нужно проходу → где лежит» —
|
||||
[project-facts.md](av-dev-pipeline/skills/review-pipeline/references/project-facts.md).
|
||||
|
||||
Прийти в старый проект и перевести его на канон — `/av-dev-pm:canon`. Канон
|
||||
@@ -191,25 +181,25 @@ EOF
|
||||
|
||||
## Снятие
|
||||
|
||||
Действие, обратное подключению. Актуально для `av-dev-backlog`: плагин устарел,
|
||||
и с каждого проекта снимается по мере перевода задач на канон `docs/tasks/`.
|
||||
|
||||
**Сначала перевод, потом снятие.** Задачи переводит `/av-dev-pm:canon`
|
||||
(`docs/backlog/` → `docs/tasks/`). Снять плагин раньше — остаться со старой
|
||||
раскладкой и без скилла, который её понимает.
|
||||
Действие, обратное подключению.
|
||||
|
||||
```bash
|
||||
cd /path/to/project
|
||||
claude plugin uninstall av-dev-backlog@av-dev-skills --scope project
|
||||
claude plugin uninstall <плагин>@av-dev-skills --scope project
|
||||
```
|
||||
|
||||
Команда правит два места: убирает строку из `enabledPlugins` в
|
||||
`.claude/settings.json` проекта и запись из реестра
|
||||
`~/.claude/plugins/installed_plugins.json`. Снимок в
|
||||
`~/.claude/plugins/cache/av-dev-skills/av-dev-backlog/<версия>/` не трогает — он
|
||||
`~/.claude/plugins/cache/av-dev-skills/<плагин>/<версия>/` не трогает — он
|
||||
общий для всех проектов. Маркетплейс тоже остаётся: `extraKnownMarketplaces`
|
||||
нужен остальным плагинам.
|
||||
|
||||
**Удалять из маркетплейса можно и до снятия с проектов.** `uninstall` идёт по
|
||||
реестру, а не по `marketplace.json`, и снимает плагин, записи о котором в
|
||||
манифесте уже нет. Проверено на `av-dev-backlog`: удалён из маркетплейса,
|
||||
снят с jellybit после — команда отработала штатно.
|
||||
|
||||
`--scope project` обязателен по той же причине, что и при установке: умолчание у
|
||||
команды — user. `cd` в проект обязателен, но здесь ошибка слышна — вызванная не
|
||||
оттуда, команда откажется словами `is not installed in project scope`, а не
|
||||
@@ -254,10 +244,6 @@ uv run pyrefly check # типы
|
||||
линтеров, и любой сторонний импорт у него не разрешается. Список запретов —
|
||||
не перечень мира, настоящий страж второй.
|
||||
|
||||
`av-dev-backlog` из проверки исключён намеренно: плагин помечен устаревшим и
|
||||
живёт до перевода последнего проекта, после чего удаляется целиком. Правки в
|
||||
замороженный код — риск без выгоды.
|
||||
|
||||
## Проверка фронтматтеров
|
||||
|
||||
Фронтматтер читает не человек, а загрузчик: по `name` он разрешает вызов, по
|
||||
@@ -273,7 +259,8 @@ uv run python scripts/frontmatter.py # 0 в порядке, 1 расхожд
|
||||
- **двоеточие с пробелом в описании без кавычек.** Для YAML `: ` внутри
|
||||
простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт,
|
||||
сверка…» — это не текст с двоеточием, а синтаксическая ошибка. Так было
|
||||
написано три описания из четырнадцати, и читались они правильно;
|
||||
написано часть описаний плагинов, и читались они правильно — замер и разбор
|
||||
в [DECISIONS.md](DECISIONS.md), решение III;
|
||||
- **`name`, разошедшееся с именем каталога скилла или файла charter'а.** Вызов
|
||||
разрешается по каталогу, а сообщение о промахе говорит «нет такого скилла»,
|
||||
а не «имя не то»;
|
||||
|
||||
+45
-22
@@ -1,7 +1,8 @@
|
||||
# Остатки, открытые вопросы и принятые пределы
|
||||
|
||||
Состояние на 2026-08-03, после разбора двенадцати тем и 16 коммитов реализации
|
||||
(`ad1779b` … `885981c`).
|
||||
Состояние пересобирается по ходу работы; счётчика тем и коммитов здесь нет
|
||||
намеренно — он протухает молча, а двигать его некому. Что и когда решено —
|
||||
[DECISIONS.md](DECISIONS.md), записи датированы.
|
||||
|
||||
План работ — [TODO.md](TODO.md). Решения с причинами — [DECISIONS.md](DECISIONS.md).
|
||||
Здесь то, что **не** является работой из плана: незакрытые риски, честно принятые
|
||||
@@ -9,16 +10,16 @@
|
||||
|
||||
## Главный незакрытый риск
|
||||
|
||||
**Калибровка не сделана, а charter'ы переписаны трижды.**
|
||||
**Калибровка не сделана, а уставы проходов с тех пор переписывались не раз.**
|
||||
|
||||
Первый раз девять charter'ов правили при выносе в плагин: предмет проверки
|
||||
заменили ссылкой на раздел брифа. `references/calibration.md` требует при такой
|
||||
правке замерить, помогла ли она, — **замера не было**. Второй раз их переписали
|
||||
коммитом `9cef452`: ссылка на раздел брифа заменена путём документа канона.
|
||||
Третий — коммитами `0eab075` и следующим, по находкам ревью: `adversary`, `ops`,
|
||||
`reimpl`, `rubric` и `triage` правились ещё раз.
|
||||
Правки шли волнами: вынос в плагин (предмет проверки заменён ссылкой на раздел
|
||||
брифа), переход на пути документов канона, две правки по находкам ревью, граф
|
||||
порядка, ступень `wide`, пересмотр триггеров ступени.
|
||||
`references/calibration.md` требует при каждой такой правке замерить, помогла ли
|
||||
она, — **ни одного замера не было**. Числа правок здесь нет намеренно: счётчик
|
||||
пришлось бы двигать вручную, и он уже однажды отстал.
|
||||
|
||||
**Три неизмеренных изменения подряд** в том самом месте, где присваивается
|
||||
**Неизмеренные изменения копятся** в том самом месте, где присваивается
|
||||
severity. Пробы готовы и синтетических не нужно — четыре реальные находки
|
||||
прошедшей сессии healthlog:
|
||||
|
||||
@@ -35,17 +36,19 @@ severity. Пробы готовы и синтетических не нужно
|
||||
но работает ли обязанность, не проверено. Оркестратор реагирует на severity,
|
||||
поэтому цена — не «не найдём», а **«найдём и не починим»**.
|
||||
|
||||
Замер стоит перед переездом jellybit и блокирует его (решение 39).
|
||||
Сама работа — [TODO.md](TODO.md), раздел 3; здесь только цена: замер стоит
|
||||
перед переездом jellybit и блокирует его (решение 39), а ожидаемый исход уже
|
||||
назван выше.
|
||||
|
||||
**«Главный» здесь про цену, а не про очередь.** Первой идёт адаптация healthlog
|
||||
(TODO, раздел 2): без неё нет проекта под каноном, на котором работают остальные
|
||||
скиллы. Калибровка блокирует один шаг — переезд jellybit, — а не всё подряд.
|
||||
|
||||
## Что ещё не сделано
|
||||
|
||||
Список работ — в [TODO.md](TODO.md). Здесь только то, что стоит держать в голове
|
||||
отдельно:
|
||||
|
||||
- **Плагины отправлены, но ни к одному проекту не подключены.** 17 коммитов
|
||||
ушли на origin, клон маркетплейса обновлён до `88c5d97` и видит `av-dev-pm`
|
||||
и `av-dev-pipeline` — то есть подключать теперь есть что. Первым делом это
|
||||
делает healthlog, по разделу 2 плана.
|
||||
- **Ни один скилл не прогонялся на живом проекте.** `docs.py` прогнан на
|
||||
healthlog и jellybit в режиме `check` и находит осмысленный дрейф; `init`,
|
||||
`canon adopt`, `canon upgrade` и скилл `docs` не исполнялись ни разу.
|
||||
@@ -57,18 +60,38 @@ severity. Пробы готовы и синтетических не нужно
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
**`doc-consistency` не различает «про нас» и «про то, что мы производим».**
|
||||
Первый прогон на самом dev-skills предъявил репозиторию правило из
|
||||
`av-dev-git/skills/commit/SKILL.md` — а это продукт, уезжающий в чужие проекты,
|
||||
а не правило, которому подчиняется маркетплейс. На проекте под каноном такой
|
||||
путаницы нет (там документы описывают сам проект), поэтому в устав это пока не
|
||||
дописано: сперва посмотреть, встретится ли класс ещё раз.
|
||||
|
||||
**Как проверять, что канон не разошёлся с проектами после `upgrade`.** `canon
|
||||
check` сверяет версию, но не то, что миграционные записи journal'а применены
|
||||
верно. Проект может нести `"canon": 2` и не иметь того, что версия 2 требовала.
|
||||
Ответ выбран: шагом 6 `upgrade` зовутся оба судьи документов — проверка не
|
||||
механическая, но других у существа записей нет. Останется открытым, пока не
|
||||
прогнано на живом проекте: неизвестно, ловят ли они недоделанную миграцию или
|
||||
только её последствия.
|
||||
|
||||
**Форма ADR при пересмотре решения.** Правило «старая запись получает статус
|
||||
`заменено на`» требует, чтобы кто-то заметил, что новое решение отменяет старое.
|
||||
Механической проверки нет, а принуждённое отрицание на шаге синка спрашивает про
|
||||
`adr/` вообще, а не «не отменяет ли это что-то из существующего».
|
||||
**Не выродились ли «границы покрытия» в шаблон.** Строка «что смотрели и чего не
|
||||
смотрели» обязательна в докладе `check`, `adopt`, интейка, штурма, сессии,
|
||||
спринта и всех четырёх агентов — семь и больше раз за сессию, и проверить её
|
||||
исполнение некому: приёмщик и исполнитель одно лицо (`session/SKILL.md`,
|
||||
«Стимулы»). Выродившаяся строка **хуже отсутствия**: доклад выглядит проверенным.
|
||||
|
||||
**Что делать с `av-dev-backlog` после перевода jellybit.** Помечен устаревшим и
|
||||
переписан так, чтобы не ловить триггер. Удалять его из маркетплейса или оставить
|
||||
как есть — решится, когда jellybit переедет.
|
||||
Приём не правится: это гипотеза об износе, а не находка, и менять работающее по
|
||||
догадке дороже. **Наблюдение к первой обкатке на живом проекте:** если в трёх
|
||||
докладах подряд границы покрытия совпали дословно или называют не то, чего
|
||||
проверка действительно не касалась, — приём выродился, и вот тогда решать.
|
||||
|
||||
**Форма ADR при пересмотре решения.** Парный статус («старая запись получает
|
||||
`заменено на`») судит агент `doc-consistency` — правило 6 его устава. Охват был
|
||||
открытым вопросом, пока агент зовётся пачкой, отобранной работой; переезд вызова
|
||||
на сессию с пачкой «весь канон» его снял. Остаётся зазор в спринт и отсутствие
|
||||
механической проверки — то есть пересмотр, сделанный сегодня, судится на
|
||||
ближайшей сессии, а не в момент правки.
|
||||
|
||||
## Известные пределы — приняты, чинить не планируется
|
||||
|
||||
|
||||
@@ -116,9 +116,10 @@
|
||||
- [ ] `canon adopt`; `docs/backlog/` → `docs/tasks/`
|
||||
- [ ] `architecture.md` 1662 строки → обзор, остаток маркерами (W)
|
||||
- [ ] после выноса поведения — замерить остаток `architecture.md` и решить по
|
||||
каталожной форме: жмёт → канон версии **4** для `architecture.md` и
|
||||
`review.md`, точка входа `README.md` (тема 16, GGG, 65; версию 3 занял
|
||||
роадмап с родом работы, тема 17, 68)
|
||||
каталожной форме: жмёт → **следующая** версия канона для
|
||||
`architecture.md` и `review.md`, точка входа `README.md` (тема 16, GGG,
|
||||
65; версию 3 занял роадмап с родом работы, тема 17, 68; версию 4 —
|
||||
секция `Сопровождение` и порядок секций, тема 26)
|
||||
- [ ] завести `security.md` с периметром первой строкой (J)
|
||||
- [ ] `review-journal.md` → `review.md` + настройка конвейера (K, L)
|
||||
- [ ] `conventions.md` → `conventions/`, `local-research.md` → `research/` (G)
|
||||
@@ -150,11 +151,11 @@
|
||||
- [ ] `docs/specs/architecture.md` → `docs/architecture.md`, `database.md` →
|
||||
`docs/database.md`, `jellyfin-layout.md` → `docs/research/`
|
||||
- [ ] `docs/review/journal.md` → `docs/review.md`
|
||||
- [ ] `drafts/` растворить: roadmap → `ROADMAP.md`, conventions-backlog → задачи
|
||||
`[idea]`, logical-title-model → ADR (H)
|
||||
- [ ] `drafts/` растворить: roadmap → `ROADMAP.md`, conventions-backlog → записи
|
||||
`research` (сырьё: тип есть, «Вопрос» пуст), logical-title-model → ADR (H)
|
||||
- [ ] `docs/backlog/` → `docs/tasks/`
|
||||
- [ ] удалить проектные копии скиллов и агентов (4 из REMAINING)
|
||||
- [ ] `av-dev-backlog` удалить из маркетплейса
|
||||
- [x] `av-dev-backlog` удалить из маркетплейса и снять с проекта (тема 30)
|
||||
|
||||
## 6. Канон версии 3 — повысить живые проекты (тема 17)
|
||||
|
||||
@@ -162,22 +163,48 @@
|
||||
jellybit 43. Шаги повышения — [changelog.md](av-dev-pm/skills/canon/references/changelog.md),
|
||||
запись «Версия 3»; делаются скиллом `av-dev-pm:canon` в режиме `upgrade`.
|
||||
|
||||
- [ ] healthlog: `PLAN.md` → `ROADMAP.md`, ссылки, `"canon": 3`
|
||||
- [x] healthlog: `PLAN.md` → `ROADMAP.md`, ссылки, `"canon": 3` — сделано,
|
||||
лежит в рабочем дереве проекта некоммитнутым
|
||||
- [ ] jellybit: то же
|
||||
- [ ] род работы и раздел «Затрагивает» — **не задним числом**: сперва то, что
|
||||
идёт в ближайший набор (`sprint take` без них откажет), остальное по ходу
|
||||
переоценки (PPP)
|
||||
- [ ] секции роадмапа: `порядок` → `Запланировано`, `темы` → `Направления`,
|
||||
завести `Готово` и `Разработка`; прозаические разделы healthlog («Что уже
|
||||
завести `Готово` и `Сопровождение`; прозаические разделы healthlog («Что уже
|
||||
пройдено», «Почему в таком порядке») разложить — звенья строками в
|
||||
`Готово`, обоснование очереди прозой внутри `Запланировано` (тема 19, 80).
|
||||
`check` теперь называет чужую секцию ошибкой, так что шаг обязателен
|
||||
- [ ] переформулировать цели ответом на «что приложение будет уметь»; цели не
|
||||
про приложение («Процесс и качество разработки» в jellybit) — в
|
||||
`Разработка`
|
||||
`Сопровождение`
|
||||
- [ ] `check --fix` на обоих: поднимет написание канонических секций, поставит
|
||||
отбивку после заголовков и сведёт секцию в мете файлов с заголовками.
|
||||
Секции беклога переименовать руками — имена выбирал проект (тема 20, ККК)
|
||||
- [ ] заголовки задач в форму действия — **не задним числом**: по мере попадания
|
||||
задачи в работу. `check` печатает их число, `task-form` предложит
|
||||
формулировки пачкой (тема 20, ЕЕЕ)
|
||||
|
||||
**Канон 4** — сверх того (changelog, запись «Версия 4»):
|
||||
|
||||
- [ ] healthlog: `## Разработка` → `## Сопровождение`, поле «Секция» в целях этой
|
||||
секции, `check --fix` (переставит `Готово` вниз и поправит отбивку),
|
||||
`"canon": 4`
|
||||
- [ ] jellybit едет сразу на 4: `Готово` заводить **последней**, секцию
|
||||
сопровождения — сразу с новым именем, переставлять дважды не нужно
|
||||
- [ ] типы: `check --fix` переведёт `kind:`/`[goal]`/`[idea]` в поле «Тип», снимет
|
||||
тег, поставит эмодзи, переименует «Секция» → «Категория» у задач и снесёт
|
||||
сырьё в конец категорий — **за один проход, вместе с порядком секций**
|
||||
- [ ] разобрать `НЕОДНОЗНАЧНО` после `--fix`: записи без типа (заведены до
|
||||
появления рода работы) машина не угадывает — `edit <слаг> --type …`
|
||||
- [ ] имена файлов: `docs.py check` назовёт кириллицу, не-kebab-case и форму
|
||||
имени ADR. Переименование ADR — **перенос ссылок одним проходом**: слаг
|
||||
стоит в `adr/README.md`, в `architecture.md` и в чужих документах
|
||||
- [ ] первый прогон `doc-consistency` на живом проекте — правило единственного
|
||||
дома до сих пор не проверял никто, урожай ожидается крупный; разбирать
|
||||
порциями
|
||||
- [ ] `doc-code-drift` — на ближайшей сессии между спринтами, с разделом
|
||||
запретов `CLAUDE.md` на входе
|
||||
- [ ] новые обязательные разделы — **не задним числом**: `Воспроизведение` у
|
||||
каждого `fix` и `Вопрос` + `Куда ляжет ответ` у каждого `research` пишутся
|
||||
по мере того, как задача идёт в набор (`sprint take` без них откажет).
|
||||
Сколько записей готово к взятию, печатает блок здоровья `check`
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "av-dev-backlog",
|
||||
"description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями, без целей и спринтов. Оставлен до перевода последнего проекта, который на нём ещё живёт; новые проекты не подключают.",
|
||||
"author": {
|
||||
"name": "Anton Vakhrushev",
|
||||
"email": "anwinged@gmail.com"
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
---
|
||||
name: backlog
|
||||
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`, плюс
|
||||
строка в индексе `README.md`. Скилл ведёт беклог: заводит, чистит, приоритизирует,
|
||||
дробит, штурмует идеи. **Реализацией не занимается** — это дело пайплайна задачи.
|
||||
|
||||
## Три правила, из которых всё следует
|
||||
|
||||
Ситуация не покрыта инструкцией — решай по ним.
|
||||
|
||||
1. **Беклог гниёт с той стороны, где его пополняют.** Заведение — самая частая
|
||||
операция и с худшим отказом: из одного разговора рождается пять файлов, и
|
||||
груминг потом разгребает то, чего не надо было заводить. Дедупликация и фильтр
|
||||
на входе дешевле любой чистки. Заводим только то, что **не делаем сейчас** и о
|
||||
потере чего пожалеем.
|
||||
2. **Файл — источник истины, индекс производен.** Разошлись — неправ индекс.
|
||||
Согласованность механизируема и проверяется командой, а не вниманием: всё, что
|
||||
ловит `backlog.py check`, не должно попадать ни в чек-лист, ни в промпт.
|
||||
3. **Причина переживает запись.** Приоритет без причины будет переспорен на
|
||||
следующем груминге; выкинутая без причины задача вернётся через квартал тем же
|
||||
текстом. Реализованная задача оставляет след в коммите и спеке — выкинутая не
|
||||
оставляет ничего, поэтому у неё есть кладбище.
|
||||
|
||||
## Инструмент (`backlog.py`)
|
||||
|
||||
Пусть `bl="$CLAUDE_PLUGIN_ROOT/skills/backlog/scripts/backlog.py"`.
|
||||
|
||||
```
|
||||
python3 $bl check # согласованность + метрики здоровья, exit 1 при расхождениях
|
||||
python3 $bl check --fix # + починить безопасный дрейф (секция, заголовок, дубли)
|
||||
python3 $bl list --stale # от самой залежавшейся; ещё --priority --type --tag
|
||||
python3 $bl add --slug S --title T --priority P [--type idea|epic] [--hook H] [--reason R] [--tag a,b]
|
||||
python3 $bl edit S [--title T] [--hook H] [--type idea|epic|task] # переименовать / сменить хук, тип
|
||||
python3 $bl move S --priority P [--reason R] # перенести в другую секцию
|
||||
python3 $bl close S --reason R # на кладбище + удалить (выкинута)
|
||||
python3 $bl close S --implemented # просто удалить (реализована, есть коммит)
|
||||
python3 $bl init [--sections "..."] # завести беклог в новом проекте
|
||||
```
|
||||
|
||||
Тип задачи — английское ключевое слово `idea` / `epic` / `task` (как и прочие
|
||||
токены команд); `task` префикса не несёт, `idea`/`epic` кодируются `[idea]`/
|
||||
`[epic]` в заголовке. Текст задачи при этом русский.
|
||||
|
||||
**Мутации правят файл и индекс заодно** — руками строку индекса или мета-строку
|
||||
не пиши, зови `add`/`edit`/`move`/`close`. Смена заголовка, хука или типа (в том
|
||||
числе понижение задачи до `[idea]`) — это `edit`, а не ручная правка H1 и
|
||||
индекса: `edit` держит их в синхроне. Механика (слаг в имени, секция по
|
||||
приоритету, формат кладбища, экранирование ввода) не может рассогласоваться,
|
||||
потому что её делает скрипт. Тело задачи скрипт не трогает — `add` кладёт
|
||||
заголовок, мета-строку и плейсхолдер, а контекст, шаги и ссылки ты дописываешь
|
||||
редактором (пока плейсхолдер на месте, `check` напоминает, что тело не дописано).
|
||||
|
||||
`check` — единственный судья согласованности; что именно он ловит, скажет его
|
||||
вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой
|
||||
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившийся
|
||||
дрейф чини `check --fix` — он детерминированно правит безопасное (секция по файлу,
|
||||
заголовок из H1, дубли строк), а неоднозначное (ссылка на исчезнувший файл,
|
||||
битые строки) выносит тебе. Это идёт строкой доклада.
|
||||
|
||||
Формат файла, мета-строки, слага, индекса и кладбища —
|
||||
[references/task-format.md](references/task-format.md). Там же тест «готова к
|
||||
взятию».
|
||||
|
||||
## Сценарии
|
||||
|
||||
### Завести задачу или идею из диалога
|
||||
|
||||
1. **Фильтр.** Делаем прямо сейчас — не заводим. Не пожалеем о потере — не
|
||||
заводим. Родилось три кандидата — покажи их и спроси, какие заводить: молча
|
||||
заведённая пачка и есть тот самый отказ из правила 1.
|
||||
2. **Дедуп.** `list` плюс поиск по слагам, хукам и телам (`grep -ril`), **включая
|
||||
`CLOSED.md`**. Нашлось в беклоге — **дописываем в существующий файл**, а не
|
||||
заводим соседний. Нашлось на кладбище — покажи пользователю ту строку и что
|
||||
изменилось с момента отказа: та же идея вернулась через диалог, а не через
|
||||
ревью. Две задачи об одном — самая дорогая находка груминга.
|
||||
3. **Тип по тесту готовности** (см. task-format): проходит — задача (`--type task`,
|
||||
без префикса), не проходит — идея (`--type idea`), проходит по пользе, но не
|
||||
делается одним заходом — эпик (`--type epic`, сперва декомпозиция).
|
||||
4. `add --slug … --title … --priority … --hook …` (тип, причину, теги — по
|
||||
месту). Хук отвечает «почему это в беклоге», а не пересказывает первый абзац.
|
||||
Затем допиши тело файла.
|
||||
5. `check`.
|
||||
|
||||
### Разобрать находки аудита или ревью
|
||||
|
||||
Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности — тоже
|
||||
источник задач, но с зеркальной диалогу опасностью: не пять файлов из одной
|
||||
мысли, а сорок файлов из сорока сырых находок. Защита та же, что в самом ревью:
|
||||
кластеризация по причине, дедуп против беклога, находка без свидетельства → идея,
|
||||
а не задача, и карта кластеров пользователю до создания файлов. Порядок и
|
||||
отображение серьёзности — [references/from-review.md](references/from-review.md).
|
||||
|
||||
### Груминг
|
||||
|
||||
Интерактивная сессия порциями, по дате правки из git и с правилом остановки —
|
||||
[references/grooming.md](references/grooming.md). Ключевое: перед вопросом
|
||||
пользователю проверь по коду и спекам, не сделано ли уже попутно, — это самая
|
||||
частая находка и она не требует ничьего решения.
|
||||
|
||||
### Приоритизация
|
||||
|
||||
Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку.
|
||||
Никаких очков и часов: уровни те, что есть в секциях индекса.
|
||||
|
||||
- Меняешь уровень — `move <slug> --priority <новый> --reason <причина>`; причина
|
||||
уезжает в мета-строку.
|
||||
- Повышаешь — назови, **что именно эта задача обгоняет**. Повышение без
|
||||
проигравшего это не приоритизация, а согласие с последним, кто говорил.
|
||||
- Задача, давно лежащая в нижней секции и не двигавшаяся (по дате git), —
|
||||
кандидат на кладбище, а не на новый круг «оставить как есть».
|
||||
|
||||
### Декомпозиция и штурм идеи
|
||||
|
||||
[references/split.md](references/split.md). Обе операции превращают одну запись в
|
||||
несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и
|
||||
**каждая давать видимую пользу**, а у штурма исход «выкинуть» — полноправный.
|
||||
|
||||
## Общее для всех сценариев
|
||||
|
||||
- **Кладбище.** Задача уходит из беклога без реализации → `close <slug> --reason
|
||||
<причина>`: скрипт пишет строку в `CLOSED.md` (дата, слаг, заголовок, причина,
|
||||
бывший приоритет) и удаляет файл со строкой индекса. Реализованные туда не идут
|
||||
— у них есть коммит, спека и ADR; для них `close <slug> --implemented`.
|
||||
- **Границы покрытия в отчёте.** Любая сессия груминга, приоритизации или штурма
|
||||
заканчивается строкой «просмотрено N из M, не трогали — …». Отчёт без неё
|
||||
сообщает «беклог разобран», не сообщая, какая его часть осталась нетронутой.
|
||||
- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным
|
||||
предварительным суждением (рекомендация — первым вариантом). Что выкинуть, что
|
||||
повысить, какая рамка идеи верна — решение пользователя. Слаг, формулировка,
|
||||
порядок строк в индексе — механика, делаем сами.
|
||||
- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа;
|
||||
решений больше — веди **несколько итераций** диалога по ≤3, а не один
|
||||
перегруженный запрос. Между итерациями применяй уже решённое.
|
||||
- **Ничего не удаляем молча.** Файл задачи исчезает только через `close` —
|
||||
`--reason` (выкинута) или `--implemented` (реализована). Прямого `rm` нет.
|
||||
|
||||
## Переносимость
|
||||
|
||||
Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего не
|
||||
знает ни про Go, ни про npm, ни про конкретный багтрекер — беклог для него просто
|
||||
каталог markdown. Текст задач — русский (язык документации проекта); зашита только
|
||||
латиница слага.
|
||||
|
||||
- **Каталог беклога**: аргумент → указатель в `CLAUDE.md` проекта → поиск
|
||||
(`docs/backlog`, `backlog`, `doc/backlog`, `docs/tasks`). Не нашёлся — это новый
|
||||
проект: `init` заводит индекс и кладбище (секции по умолчанию высокий/средний/
|
||||
низкий, `--sections` переопределяет).
|
||||
- **Слаг** — латиница kebab-case всегда; заголовок, тело, хук — по-русски.
|
||||
- **Уровни приоритета** берутся из заголовков секций индекса как есть, их
|
||||
количество и названия — дело проекта.
|
||||
- **Имена служебных файлов** (`README.md` — индекс, `CLOSED.md` — кладбище)
|
||||
фиксированы скиллом, не проектом.
|
||||
|
||||
Проектные тонкости (куда переезжает суть реализованной задачи, кто удаляет файл,
|
||||
как беклог связан с трекером-инбоксом) описаны в `CLAUDE.md` проекта — прочитай
|
||||
его перед работой.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
Не пишет код, не заводит спеки и change, не берёт задачу в работу — этим
|
||||
занимается пайплайн задачи проекта, а этот скилл владеет только форматом и
|
||||
содержимым беклога. Не решает за пользователя, что важно. Не переоформляет
|
||||
существующие задачи «заодно»: правится то, чего касается операция.
|
||||
@@ -1,84 +0,0 @@
|
||||
# Задачи из аудита и ревью
|
||||
|
||||
Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности, любой
|
||||
разбор другим агентом — порождают находки, часть которых становится задачами
|
||||
беклога. Это отдельный интейк со своей опасностью, **зеркальной** интейку из
|
||||
диалога.
|
||||
|
||||
- Интейк из диалога грешит переполнением: из одной мысли рождается пять файлов.
|
||||
- Интейк из ревью грешит сваливанием: сорок сырых находок превращаются в сорок
|
||||
файлов. Беклог раздувается, а следующий груминг склеивает их обратно.
|
||||
|
||||
Защита от сваливания — та же, что в самом ревью: **кластеризация по причине, а не
|
||||
файл-на-находку.** Если у ревью был триаж — половина работы уже сделана, бери его
|
||||
выход. Если нет — триажируй сам, прежде чем заводить.
|
||||
|
||||
## Находка агента — не задача
|
||||
|
||||
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
|
||||
воспроизводимый шаг, положение гайда). Согласие нескольких находок само по себе
|
||||
достоверность не повышает: это один источник, высказавшийся несколько раз.
|
||||
|
||||
Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»:
|
||||
|
||||
- **Находка со свидетельством**, отложенная к исполнению → **задача**.
|
||||
Свидетельство и последствие переносим в тело — это её «почему», то самое, что
|
||||
переживает запись.
|
||||
- **Находка без свидетельства / низкой уверенности** → **идея** (`[idea]`), а не
|
||||
задача. Она не заработала приоритизацию: сравнивать неподтверждённое не с чем.
|
||||
Её судьба — штурм, где либо найдётся подтверждение, либо она уедет на кладбище.
|
||||
- **Уже починено по ходу ревью** → **ничего**. Починенное не заводим.
|
||||
- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с
|
||||
зафиксированным вопросом.
|
||||
|
||||
## Порядок
|
||||
|
||||
1. **Возьми выход триажа, а не сырые находки.** Сырой отчёт — это симптомы до
|
||||
дедупликации; в нём одна причина размазана по нескольким строкам.
|
||||
2. **Кластеризуй по причине.** Пять находок об одном отсутствующем инварианте —
|
||||
одна задача, а не пять. Класс мелочи (nits, косметика) — **один пакетный файл**
|
||||
со списком пунктов, а не файл на каждую запятую.
|
||||
3. **Дедуп против беклога и кладбища.** Аудит переоткрывает уже заведённое и уже
|
||||
выкинутое. Нашлось в беклоге — дописываем находку в существующий файл. Нашлось
|
||||
на кладбище — это сигнал: причина отказа могла устареть, выноси пользователю, а
|
||||
не заводи молча заново.
|
||||
4. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в
|
||||
пакетный файл / уже в беклоге / отброшено — пачкой через `AskUserQuestion`.
|
||||
Это тот же барьер, что и «три кандидата» в интейке из диалога: массовое
|
||||
заведение файлов без подтверждения — ровно тот отказ, ради которого интейк из
|
||||
ревью и выделен. Дешёвая мелочь по явному согласию может заводиться и без
|
||||
поштучного вопроса — но карта пользователю всё равно предъявляется.
|
||||
5. **Заводи утверждённое** через `backlog.py add`, с двумя добавками:
|
||||
- **тег партии** — `add … --tag review-ГГГГ-ММ-ДД` (или `audit-<тема>`), чтобы
|
||||
весь заход груминга поднимался одной командой `backlog.py list --tag …`;
|
||||
- **провенанс в теле** — кто нашёл, каким проходом, с каким свидетельством. Без
|
||||
него через месяц не отличить проверенную находку от догадки.
|
||||
6. `backlog.py check`.
|
||||
|
||||
## Отображение серьёзности на приоритет
|
||||
|
||||
Правило концептуальное, от полей конкретного отчёта не зависит:
|
||||
|
||||
- **выше серьёзность → выше приоритет.** Самый тяжёлый класс находок → верхняя
|
||||
секция индекса, следующий → следующая. Отображать словарь серьёзности отчёта на
|
||||
словарь приоритетов проекта точно нечем — при сомнении спрашивай пользователя.
|
||||
- **низкая уверенность или нет свидетельства → идея**, не задача.
|
||||
- **мелочь → строка в пакетный файл**, не отдельный.
|
||||
- **уже починено / развилка решена сейчас → ничего.**
|
||||
|
||||
Если у ревью структурированный отчёт с полями серьёзности, уверенности,
|
||||
свидетельства и предписанного действия (например, конвейер ревью jellybit даёт
|
||||
`Severity`/`Confidence`/`Оракул`/`Действие: инлайн|развилка`) — правило выше
|
||||
ложится на эти поля механически. Но это пример одного формата, а не требование к
|
||||
источнику: тот же фильтр применяется к находкам в свободной форме.
|
||||
|
||||
Границы покрытия отчёта — то, что ревью проверить **не смогло**, — не находки и в
|
||||
задачи не идут: у них нет предмета. Их место — в докладе, не в беклоге.
|
||||
|
||||
## Доклад
|
||||
|
||||
- Источник (какое ревью/аудит, сколько находок на входе).
|
||||
- Свёрнуто в задачи: N кластеров из M находок, со слагами и тегом партии.
|
||||
- Что не заведено и почему: починено инлайн, уже в беклоге, ушло в идеи, на
|
||||
кладбище.
|
||||
- `backlog.py check`.
|
||||
@@ -1,101 +0,0 @@
|
||||
# Груминг беклога
|
||||
|
||||
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
|
||||
состояние. Не «пересмотреть всё», а «пересмотреть порцию до конца».
|
||||
|
||||
## Порция и правило остановки
|
||||
|
||||
Тридцать задач за один заход — это усталость и штамповка: последние десять
|
||||
получат «оставить» не потому, что живы, а потому, что сессия затянулась.
|
||||
|
||||
- **5–8 задач за сессию.** Больше — только если пользователь настаивает, и тогда
|
||||
разбей на явные порции с промежуточным докладом.
|
||||
- **Отбор порции** — один из:
|
||||
- `backlog.py list --stale` — самые залежавшиеся по дате последней правки в
|
||||
git; поле «дата касания» заводить не надо, git её уже хранит;
|
||||
- одна секция приоритета целиком;
|
||||
- один тег (`--tag`) — например, задачи, пришедшие из одного ревью;
|
||||
- список от пользователя.
|
||||
- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось».
|
||||
|
||||
## Что делать с каждой задачей
|
||||
|
||||
Сперва то, что не требует ничьего решения:
|
||||
|
||||
1. **Проверь, не сделано ли уже.** Задача, реализованная попутно в соседнем
|
||||
изменении, — самая частая находка груминга. Смотри код, спеки, историю
|
||||
коммитов по ключевым словам задачи. Удаление задачи «как реализованной» —
|
||||
деструктивно и без следа (кладбище для реализованных не пишется), поэтому
|
||||
порог улики жёсткий: удаляем (`close <slug> --implemented`), только имея
|
||||
**конкретный коммит или строку спеки**, закрывающие задачу, и ссылка на них
|
||||
идёт в доклад. Есть лишь косвенные признаки — не удаляй сам, вынеси в пачку
|
||||
вопросов. Сделана частично → задача сжимается до остатка: тело правишь
|
||||
редактором, заголовок и хук — через `edit <slug> --title … --hook …`.
|
||||
2. **Проверь, не отменена ли решением.** ADR, спека или архивный change мог
|
||||
закрыть вопрос иначе. Тогда `close <slug> --reason "<ссылка на решение>"`.
|
||||
3. **Проверь пересечения внутри порции.** Две задачи об одном — содержимое в
|
||||
одну, вторую `close <slug> --reason "слита с <другой-slug>"`.
|
||||
|
||||
Затем — то, что решает пользователь:
|
||||
|
||||
4. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
|
||||
сценарий, обошли иначе. Здесь и звучит вопрос о выкидывании.
|
||||
5. **Тот ли приоритет** (тест и правила — в SKILL.md и task-format.md).
|
||||
6. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit
|
||||
<slug> --type idea`, и её дальнейшая судьба — штурм, а не приоритизация.
|
||||
Разрослась → `edit <slug> --type epic`, дальше декомпозиция.
|
||||
|
||||
## Храповик
|
||||
|
||||
Сильно залежавшаяся задача — сигнал сама по себе: её либо ни разу не собирались
|
||||
делать, либо нечем взять. Измеряй наблюдаемым — датой последней правки из git
|
||||
(`backlog.py list --stale` ставит такие первыми); счётчик «сколько грумингов
|
||||
пережила» нигде не хранится, поэтому на него не опирайся.
|
||||
|
||||
Задача из верхних строк `--stale`, которую и этот заход оставляет без изменений,
|
||||
**либо двигается (вверх или на кладбище), либо остаётся с явно записанной
|
||||
причиной**, почему её держим (`move <slug> --priority <тот же> --reason …`).
|
||||
Молчаливое «оставить как есть» на давно неподвижной задаче — это решение не
|
||||
принимать решение; запись причины превращает его в осознанное и не даёт тому же
|
||||
вопросу всплыть на следующем груминге. В примере ниже вариант «оставить» именно
|
||||
такой — с названной причиной, а не по умолчанию.
|
||||
|
||||
## Интерактив
|
||||
|
||||
- Вопросы — через `AskUserQuestion`, **не больше трёх за раз**. Порция в 5–8
|
||||
задач обычно даёт больше трёх суждений — тогда веди несколько итераций по ≤3, а
|
||||
не по одному на задачу и не одним перегруженным запросом.
|
||||
- К каждому варианту — **предварительное суждение**, рекомендация первым
|
||||
вариантом: «предлагаю выкинуть, потому что …». Пользователю дешевле возразить,
|
||||
чем судить с нуля.
|
||||
- Всё, что решается фактом (сделано / отменено / дублируется), решай сам и
|
||||
показывай списком в докладе, а не выноси в вопросы.
|
||||
|
||||
Пример одной итерации — три залежавшихся задачи, механику по ним уже разобрали:
|
||||
|
||||
> **Груминг: 3 залежавшихся (порция по `--stale`)**
|
||||
>
|
||||
> 1. `versii-kachestvo-repaki` — репаки, апгрейд 1080p→2160p
|
||||
> - Выкинуть на кладбище *(рекомендую)* — помечена «не боль», за полгода ни разу не возникла
|
||||
> - Оставить в низком
|
||||
> - Поднять в средний
|
||||
> 2. `backup-sqlite` — бэкап SQLite
|
||||
> - Оставить в среднем *(рекомендую)* — не сработала, но риск реальный
|
||||
> - Поднять в высокий — обгоняет `retention-ochistka-bd`: без бэкапа ретеншн опасен
|
||||
> - Выкинуть
|
||||
> 3. `guessit-sputnik` — guessit как сервис-спутник
|
||||
> - Понизить до `[idea]` *(рекомендую)* — не проходит тест «готова к взятию»
|
||||
> - Оставить задачей в низком
|
||||
|
||||
Каждый вариант несёт причину — ту самую, что уедет в `move --reason` или
|
||||
`close --reason`. Ответы применяй сразу и, если в порции осталось ещё, следующей
|
||||
итерацией показывай следующие ≤3.
|
||||
|
||||
## Доклад
|
||||
|
||||
- Что просмотрено: N из M, по какому признаку отобрана порция.
|
||||
- Изменения списком: удалено (реализовано), на кладбище (с причинами), понижено
|
||||
до идей, слито, переприоритизировано.
|
||||
- **Границы покрытия**: сколько задач не трогали и какие именно секции или теги
|
||||
остались — иначе доклад читается как «беклог разобран».
|
||||
- `backlog.py check` после правок; результат — строкой в докладе.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Декомпозиция и мозговой штурм
|
||||
|
||||
Обе операции превращают одну запись беклога в несколько (или в ноль). Разница в
|
||||
входе: декомпозиция дробит **готовую задачу**, штурм прорабатывает **идею**,
|
||||
которая ещё не задача.
|
||||
|
||||
## Тест декомпозиции
|
||||
|
||||
Задачу можно дробить, только если части удовлетворяют **обоим** условиям:
|
||||
|
||||
1. **Мерджатся независимо.** Часть Б не требует, чтобы часть А была уже влита.
|
||||
Есть порядок «сперва А, потом Б, иначе не собрать» → это не декомпозиция, а
|
||||
план реализации: шаги остаются **внутри одного файла** в разделе «Шаги».
|
||||
2. **Каждая даёт видимую пользу.** Часть, полезная только в комплекте с другой, —
|
||||
не самостоятельная задача. Пользу проверяй тем же тестом «готова к взятию»
|
||||
(task-format): что станет наблюдаемо иначе именно от этой части.
|
||||
|
||||
Не проходит хотя бы одно — **не дроби**. Ложная декомпозиция плодит файлы,
|
||||
которые нельзя взять поодиночке, и груминг потом их склеивает обратно.
|
||||
|
||||
## Что делать с родителем
|
||||
|
||||
После разделения родитель **не остаётся** третьей висящей строкой:
|
||||
|
||||
- части полностью замещают его → `close <slug> --reason "разложена на a, b"`.
|
||||
Кладбище здесь — не «выкинули», а именно тот след, что переживает запись:
|
||||
через квартал вопрос «куда делась задача X» отвечается строкой кладбища со
|
||||
ссылками на наследников, а не археологией git;
|
||||
- родитель осмыслен как зонтик → `edit <slug> --type epic`, тело — ссылки на
|
||||
задачи-части, своих шагов у него нет.
|
||||
|
||||
Одно и то же не должно лежать и в родителе, и в части. Задвоение — то же
|
||||
расхождение, что ловит `check`, только внутри тел.
|
||||
|
||||
## Мозговой штурм идеи
|
||||
|
||||
Идея (`[idea]`) не проходит тест «готова к взятию»: неясно, что именно делаем.
|
||||
Штурм проясняет — и это **generative-операция, а не applicative**.
|
||||
|
||||
Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное:
|
||||
перечисляется то, что уже видно в формулировке. Ценное — на уровень выше.
|
||||
|
||||
1. **Сперва — формы, а не задачи.** Предложи **три разные постановки** идеи и
|
||||
назови **компромисс каждой**: что она даёт, чем платит, что оставляет за
|
||||
бортом. Если получилась одна постановка — штурм не состоялся, это applicative.
|
||||
2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку
|
||||
выбирает он: это продуктовое решение, не механика.
|
||||
3. **Только выбранную форму** дроби по тесту декомпозиции выше.
|
||||
|
||||
**«Выкинуть» — полноправный исход штурма, а не его неудача.** Проработка, честно
|
||||
показавшая, что пользы нет или она несоразмерна цене, — это результат: идея
|
||||
уезжает на кладбище с этой самой причиной, и та причина гасит её повторное
|
||||
появление.
|
||||
|
||||
## Доклад
|
||||
|
||||
- Идея/задача на входе, выбранная рамка (для штурма), задачи-наследники со
|
||||
слагами и приоритетами.
|
||||
- Судьба родителя: удалён / стал эпиком / выкинут.
|
||||
- `backlog.py check` после правок.
|
||||
- Границы покрытия: какие постановки рассмотрены и какие сознательно отброшены —
|
||||
чтобы штурм не пришлось повторять с нуля.
|
||||
@@ -1,104 +0,0 @@
|
||||
# Формат беклога
|
||||
|
||||
Заголовок, мета-строку и строку индекса ставит `backlog.py add` — руками их не
|
||||
пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`;
|
||||
тело задачи (контекст, шаги, ссылки) дописывает агент.
|
||||
|
||||
## Файл задачи
|
||||
|
||||
`<slug>.md` в каталоге беклога:
|
||||
|
||||
```markdown
|
||||
# Раздачи с докачиванием (merge при повторном добавлении)
|
||||
|
||||
**Приоритет:** высокий — блокирует типовой сценарий свежих сериалов · **Теги:** layout, ingest
|
||||
|
||||
Свежий сериал раздают по мере выхода: торрент с 5 из 10 эпизодов позже
|
||||
перезаливают целиком, пользователь добавляет раздачу повторно. …
|
||||
|
||||
Шаги:
|
||||
- в плане раскладки отличать «путь занят живой ссылкой того же матча» от коллизии
|
||||
- merge-раскладка: существующее пропустить, недостающее доложить
|
||||
|
||||
Зависит от правила сходимости. Связано: drafts/logical-title-model.md §6.2.
|
||||
```
|
||||
|
||||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
|
||||
префиксом `[idea]` / `[epic]`; обычная задача без префикса. Отдельного поля
|
||||
типа **нет**: два места для одного факта разъезжаются, а префикс виден прямо в
|
||||
индексе, где и принимается решение «брать или не брать».
|
||||
- **Мета-строка** — первая непустая строка после заголовка. Обязателен приоритет,
|
||||
причина после тире желательна, теги опциональны. Поля разделяются ` · `, их
|
||||
порядок свободный. `·` — служебный разделитель: в тексте причины его быть не
|
||||
должно, иначе причина обрежется по нему.
|
||||
- **Тело** — контекст (почему это вообще задача), принятые решения, шаги,
|
||||
ссылки на спеки, ADR, черновики, прошлые ревью. Пишется на языке документации
|
||||
проекта.
|
||||
|
||||
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
|
||||
в документацию проекта, а файл задачи удаляется.
|
||||
|
||||
## Слаг
|
||||
|
||||
Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов
|
||||
(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути задачи, а не по текущей
|
||||
формулировке**: заголовок будет переписан на груминге, а слаг стоит в ссылках из
|
||||
других задач, коммитов и черновиков. Транслит русского названия допустим, если
|
||||
суть иначе не выражается коротко.
|
||||
|
||||
## Индекс
|
||||
|
||||
`README.md` в том же каталоге: преамбула, затем секции по приоритетам, в каждой —
|
||||
строки вида
|
||||
|
||||
```markdown
|
||||
- [Заголовок задачи дословно](slug.md) — хук
|
||||
```
|
||||
|
||||
Хук отвечает на «почему это лежит в беклоге» одним предложением: состояние,
|
||||
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
|
||||
|
||||
Порядок секций задаёт порядок приоритетов, их названия — единственный словарь
|
||||
уровней. Внутри секции порядок значения не имеет. Секции приоритетов — **единственные
|
||||
заголовки `##` в индексе**: любой другой `##` в преамбуле проверка сочтёт уровнем
|
||||
приоритета.
|
||||
|
||||
Индекс **производен**: расходится с файлом — правим индекс. Строку индекса руками
|
||||
не пишут — её ставит `backlog.py add` в секцию приоритета и двигает `move`.
|
||||
|
||||
## Кладбище — `CLOSED.md`
|
||||
|
||||
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
|
||||
`backlog.py close --reason`, а `check` следит за её форматом:
|
||||
|
||||
```markdown
|
||||
- 2026-07-23 `versii-kachestvo-repaki` — Версии/качество одного тайтла (репаки,
|
||||
апгрейд 1080p → 2160p). Причина: калибровка болей — не боль, ни разу не
|
||||
возникло за полгода. Был приоритет: низкий.
|
||||
```
|
||||
|
||||
Реализованные сюда не попадают: у них остаётся коммит, спека, ADR. У выкинутой не
|
||||
остаётся ничего — и через квартал она возвращается тем же текстом через инбокс.
|
||||
Кладбище — первое место, куда смотрит дедупликация при заведении.
|
||||
|
||||
Запись на кладбище не запрещает завести задачу заново: изменился контекст —
|
||||
заводим и ссылаемся на строку кладбища, объясняя, что изменилось.
|
||||
|
||||
## Тест «готова к взятию»
|
||||
|
||||
Задача готова, если из файла отвечаются три вопроса:
|
||||
|
||||
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
|
||||
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
|
||||
ломаться Y при Z» — ответ.
|
||||
2. **По чему видно, что закончено.** Признак завершённости, а не список работ.
|
||||
3. **Почему приоритет такой** — одна строка.
|
||||
|
||||
Не отвечается первый или второй вопрос → это **идея**, её место в штурме, а не в
|
||||
приоритизации. Приоритизировать идеи бессмысленно: сравнивается неизвестно что.
|
||||
|
||||
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
|
||||
**эпик**, сперва декомпозиция.
|
||||
|
||||
Тест применяется при заведении и на груминге. К старым задачам, которых операция
|
||||
не касается, задним числом не применяется — беклог не переоформляют «заодно».
|
||||
@@ -1,706 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Детерминированный инструмент беклога: файлы задач против индекса README.
|
||||
|
||||
Согласованность беклога — механизируемая вещь, и держать её вниманием агента
|
||||
дорого и ненадёжно. Скрипт не только проверяет, но и **пишет**: создание,
|
||||
переименование, перенос между приоритетами и закрытие правят файл и индекс
|
||||
заодно, так что рассогласовать их вручную нельзя. Всё, что здесь механизировано,
|
||||
не должно попадать ни в промпт, ни в чек-лист человека.
|
||||
|
||||
Источник истины — файл задачи. Индекс производен от файлов: расходятся —
|
||||
неправ индекс.
|
||||
|
||||
Тип задачи — ключевое слово (idea | epic | task); по-английски, как и прочие
|
||||
токены команд. Обычная задача (task) префикса не несёт, idea/epic кодируются
|
||||
префиксом `[idea]`/`[epic]` в заголовке. Текст самой задачи — русский.
|
||||
|
||||
Использование:
|
||||
backlog.py check [--dir DIR] [--fix] согласованность (+ здоровье беклога);
|
||||
--fix чинит безопасный дрейф
|
||||
backlog.py list [--dir DIR] [фильтры] список задач
|
||||
--stale от самой залежавшейся (дата последней правки из git)
|
||||
--priority СЛОВО / --type idea|epic / --tag СЛОВО фильтры
|
||||
backlog.py add --slug S --title T --priority P [--type idea|epic]
|
||||
[--hook H] [--reason R] [--tag a,b] [--dir DIR]
|
||||
создать задачу: файл + строка индекса
|
||||
backlog.py edit S [--title T] [--hook H] [--type idea|epic|task] [--dir DIR]
|
||||
сменить заголовок/хук/тип (файл + индекс)
|
||||
backlog.py move S --priority P [--reason R] [--dir DIR]
|
||||
перенести в другую секцию приоритета
|
||||
backlog.py close S (--reason R | --implemented) [--dir DIR]
|
||||
закрыть: --reason → кладбище + удаление,
|
||||
--implemented → просто удаление (есть коммит)
|
||||
backlog.py init [--dir DIR] [--sections "высокий,средний,низкий"]
|
||||
завести пустой беклог в новом проекте
|
||||
|
||||
Тело задачи (контекст, шаги, ссылки) остаётся агенту — add кладёт лишь заголовок,
|
||||
мета-строку и плейсхолдер; агент дописывает тело редактором.
|
||||
|
||||
Границы безопасности: слаг — только латиница kebab-case (traversal невозможен),
|
||||
--dir обязан быть внутри рабочего каталога, в заголовок/хук/причину не пролезет
|
||||
перевод строки, `·` в причине запрещён (это разделитель мета-полей).
|
||||
|
||||
Язык не зашит инструментально: приоритеты сопоставляются с заголовками секций
|
||||
индекса как есть. Текст задач — русский.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
INDEX = "README.md"
|
||||
CLOSED = "CLOSED.md"
|
||||
SERVICE = {INDEX, CLOSED}
|
||||
|
||||
META_FIELD = re.compile(r"^\*\*(.+?):\*\*\s*(.*)$")
|
||||
INDEX_ENTRY = re.compile(r"^- \[(.+?)\]\((.+?\.md)\)\s*(?:—\s*(.*))?$")
|
||||
SECTION = re.compile(r"^##\s+(.+?)\s*$")
|
||||
TYPE_PREFIX = re.compile(r"^\[(.+?)\]\s*(.*)$")
|
||||
SLUG_RE = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
|
||||
SLUG = re.compile(SLUG_RE.pattern + r"\.md")
|
||||
# Строка кладбища: - ГГГГ-ММ-ДД `slug` — текст
|
||||
CLOSED_ENTRY = re.compile(r"^- \d{4}-\d{2}-\d{2} `[a-z0-9-]+` — .+")
|
||||
|
||||
TYPES = ("idea", "epic") # непустые типы-ключевые слова, префикс [..] в H1
|
||||
PLAIN_TYPE = "task" # обычная задача — без префикса
|
||||
STALE_DAYS = 180 # порог «залежалась» для метрики здоровья в check
|
||||
|
||||
|
||||
# --- Валидация недоверенного ввода (аргументы могут прийти из текста задачи) ---
|
||||
|
||||
def bad_line(value: str, field: str) -> str | None:
|
||||
"""Однострочность: перевод строки/управляющий символ ломает индекс и файл."""
|
||||
if value is not None and (any(c in value for c in "\n\r") or any(ord(c) < 32 for c in value)):
|
||||
return f"{field}: перевод строки или управляющий символ запрещён"
|
||||
return None
|
||||
|
||||
|
||||
def bad_slug(slug: str) -> str | None:
|
||||
if not SLUG_RE.fullmatch(slug):
|
||||
return f"слаг «{slug}» — только латиница kebab-case (без ../, точек, слэшей)"
|
||||
return None
|
||||
|
||||
|
||||
def bad_reason(reason: str | None) -> str | None:
|
||||
if reason is None:
|
||||
return None
|
||||
if (e := bad_line(reason, "причина")):
|
||||
return e
|
||||
if "·" in reason:
|
||||
return "причина: символ · зарезервирован под разделитель мета-полей"
|
||||
return None
|
||||
|
||||
|
||||
def dir_within_cwd(root: Path) -> bool:
|
||||
try:
|
||||
root.resolve().relative_to(Path.cwd().resolve())
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
# --- Атомарная запись: падение посреди write не оставит усечённый индекс ---
|
||||
|
||||
def write_atomic(path: Path, text: str) -> None:
|
||||
tmp = path.with_name(path.name + ".tmp")
|
||||
tmp.write_text(text, encoding="utf-8")
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
def resolve_dir(explicit: str | None) -> Path:
|
||||
"""Каталог беклога для команд, кроме init. Явный --dir обязан быть внутри cwd."""
|
||||
if explicit:
|
||||
root = Path(explicit)
|
||||
if not dir_within_cwd(root):
|
||||
sys.exit(f"--dir вне рабочего каталога: {explicit}")
|
||||
if not (root / INDEX).is_file():
|
||||
sys.exit(f"беклога нет в «{explicit}» (нет {INDEX}); новый проект — backlog.py init")
|
||||
return root
|
||||
for candidate in ("docs/backlog", "backlog", "doc/backlog", "docs/tasks"):
|
||||
if (Path(candidate) / INDEX).is_file():
|
||||
return Path(candidate)
|
||||
sys.exit("каталог беклога не найден, укажи --dir"
|
||||
" (искал: docs/backlog, backlog, doc/backlog, docs/tasks)")
|
||||
|
||||
|
||||
def parse_index(root: Path) -> tuple[dict[str, dict], list[str]]:
|
||||
"""Строки индекса по имени файла + порядок секций (он же порядок приоритетов).
|
||||
|
||||
Дубли имени файла тут схлопываются (побеждает последний) — их отдельно ловит
|
||||
index_lint, поэтому опираться на этот dict как на полноту нельзя.
|
||||
"""
|
||||
entries: dict[str, dict] = {}
|
||||
sections: list[str] = []
|
||||
section = None
|
||||
for num, line in enumerate((root / INDEX).read_text(encoding="utf-8").splitlines(), 1):
|
||||
m = SECTION.match(line)
|
||||
if m:
|
||||
section = m.group(1)
|
||||
sections.append(section)
|
||||
continue
|
||||
m = INDEX_ENTRY.match(line)
|
||||
if m:
|
||||
title, target, hook = m.group(1), m.group(2), (m.group(3) or "").strip()
|
||||
entries[target] = {"title": title, "section": section, "hook": hook, "line": num}
|
||||
return entries, sections
|
||||
|
||||
|
||||
def index_lint(root: Path) -> list[str]:
|
||||
"""Структурные дефекты индекса, которые схлопнутый dict parse_index не видит:
|
||||
битые строки-пункты, дубли на один файл, задачи до первой секции приоритета."""
|
||||
errors: list[str] = []
|
||||
section = None
|
||||
seen: dict[str, int] = {}
|
||||
for num, line in enumerate((root / INDEX).read_text(encoding="utf-8").splitlines(), 1):
|
||||
if SECTION.match(line):
|
||||
section = SECTION.match(line).group(1)
|
||||
continue
|
||||
if not line.startswith("- ["):
|
||||
continue
|
||||
m = INDEX_ENTRY.match(line)
|
||||
if not m:
|
||||
errors.append(f"{INDEX}:{num}: строка-пункт не по формату"
|
||||
f" «- [Заголовок](slug.md) — хук»")
|
||||
continue
|
||||
target = m.group(2)
|
||||
if section is None:
|
||||
errors.append(f"{INDEX}:{num}: {target} стоит до первой секции приоритета")
|
||||
if target in seen:
|
||||
errors.append(f"{INDEX}:{num}: дубль строки для {target}"
|
||||
f" (первая — строка {seen[target]})")
|
||||
else:
|
||||
seen[target] = num
|
||||
return errors
|
||||
|
||||
|
||||
def parse_task(path: Path) -> dict:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
title = lines[0].removeprefix("#").strip() if lines and lines[0].startswith("#") else ""
|
||||
kind, bare = PLAIN_TYPE, title
|
||||
m = TYPE_PREFIX.match(title)
|
||||
if m:
|
||||
kind, bare = m.group(1).strip().lower(), m.group(2).strip()
|
||||
# Мета-строка — первая непустая строка после заголовка (task-format.md).
|
||||
# Поля разделены `·`, порядок свободный: приоритет распознаётся, где бы он ни
|
||||
# стоял, а не только первым. Причина не должна содержать `·` — это разделитель.
|
||||
meta = next((ln.strip() for ln in lines[1:] if ln.strip()), "")
|
||||
priority, reason, tags = "", "", []
|
||||
if META_FIELD.match(meta):
|
||||
for chunk in meta.split("·"):
|
||||
f = META_FIELD.match(chunk.strip())
|
||||
if not f:
|
||||
continue
|
||||
key, value = f.group(1).strip().lower(), f.group(2).strip()
|
||||
if key in ("приоритет", "priority"):
|
||||
priority, _, reason = (p.strip() for p in value.partition("—"))
|
||||
priority = priority.rstrip(".,").lower()
|
||||
elif key in ("теги", "tags"):
|
||||
tags = [t.strip().lower() for t in value.split(",") if t.strip()]
|
||||
return {"title": title, "bare": bare, "type": kind, "priority": priority,
|
||||
"reason": reason, "tags": tags, "path": path}
|
||||
|
||||
|
||||
def tasks_of(root: Path) -> dict[str, dict]:
|
||||
return {p.name: parse_task(p) for p in sorted(root.glob("*.md")) if p.name not in SERVICE}
|
||||
|
||||
|
||||
def touched_map(root: Path) -> dict[str, str]:
|
||||
"""Дата последнего коммита для каждого файла беклога — одним вызовом git.
|
||||
Ключ — имя файла (в каталоге беклога имена уникальны). Нет git / нет
|
||||
истории → пустая карта, вызывающий подставит «—»."""
|
||||
try:
|
||||
out = subprocess.run(["git", "log", "--format=%as", "--name-only", "--", str(root)],
|
||||
capture_output=True, text=True).stdout
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
dates: dict[str, str] = {}
|
||||
cur = None
|
||||
for line in out.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", line):
|
||||
cur = line # лог новейшие сверху → первая дата и есть последняя правка
|
||||
elif cur:
|
||||
dates.setdefault(os.path.basename(line), cur)
|
||||
return dates
|
||||
|
||||
|
||||
def check(root: Path, fix: bool = False) -> int:
|
||||
if fix:
|
||||
for line in apply_fixes(root):
|
||||
print(f"ПОЧИНЕНО {line}")
|
||||
print()
|
||||
|
||||
entries, sections = parse_index(root)
|
||||
tasks = tasks_of(root)
|
||||
known = {s.lower() for s in sections}
|
||||
errors: list[str] = []
|
||||
notes: list[str] = []
|
||||
|
||||
for name, task in tasks.items():
|
||||
entry = entries.get(name)
|
||||
if not entry:
|
||||
errors.append(f"{name}: файла нет в индексе {INDEX}")
|
||||
if not SLUG.fullmatch(name):
|
||||
errors.append(f"{name}: слаг не kebab-case латиницей")
|
||||
if not task["title"]:
|
||||
errors.append(f"{name}: нет заголовка H1")
|
||||
if not task["priority"]:
|
||||
errors.append(f"{name}: нет строки **Приоритет:**")
|
||||
elif task["priority"] not in known:
|
||||
errors.append(f"{name}: приоритет «{task['priority']}» не совпадает"
|
||||
f" ни с одной секцией индекса ({', '.join(sections)})")
|
||||
elif entry and entry["section"] and entry["section"].lower() != task["priority"]:
|
||||
errors.append(f"{name}: приоритет в файле «{task['priority']}»,"
|
||||
f" а в индексе секция «{entry['section']}»")
|
||||
if entry and entry["title"] != task["title"]:
|
||||
errors.append(f"{name}: заголовок разошёлся\n"
|
||||
f" файл: {task['title']}\n"
|
||||
f" индекс: {entry['title']}")
|
||||
if entry and not entry["hook"]:
|
||||
notes.append(f"{name}: строка индекса без хука — по ней не выбрать задачу")
|
||||
if task["type"] not in TYPES and task["type"] != PLAIN_TYPE:
|
||||
notes.append(f"{name}: тип «{task['type']}» вне словаря"
|
||||
f" ({'/'.join(TYPES)} или без префикса)")
|
||||
if "<!-- контекст" in task["path"].read_text(encoding="utf-8"):
|
||||
notes.append(f"{name}: тело не дописано (остался плейсхолдер add)")
|
||||
|
||||
for name, entry in entries.items():
|
||||
if name not in tasks:
|
||||
errors.append(f"{INDEX}:{entry['line']}: ссылка на несуществующий {name}")
|
||||
|
||||
errors += index_lint(root)
|
||||
|
||||
# Кладбище: строки-пункты должны совпадать с форматом (его пишет close).
|
||||
closed = root / CLOSED
|
||||
if closed.is_file():
|
||||
for num, line in enumerate(closed.read_text(encoding="utf-8").splitlines(), 1):
|
||||
if line.startswith("- ") and not CLOSED_ENTRY.match(line):
|
||||
errors.append(f"{CLOSED}:{num}: строка кладбища не по формату"
|
||||
f" «- ГГГГ-ММ-ДД `slug` — …»")
|
||||
|
||||
# Причина у приоритета желательна, но не обязательна. Ругаемся только на
|
||||
# частичное покрытие — это дрейф: у части задач причина есть, у части нет.
|
||||
# Ноль из N — осознанный отказ проекта от причин, не расхождение; горящее на
|
||||
# каждом check замечание агент просто научится игнорировать.
|
||||
with_reason = sum(1 for t in tasks.values() if t["reason"])
|
||||
if 0 < with_reason < len(tasks):
|
||||
notes.append(f"причина у приоритета есть у {with_reason} из {len(tasks)}"
|
||||
f" — либо у всех, либо ни у кого: вперемешку это дрейф")
|
||||
|
||||
print(f"беклог: {root}, задач {len(tasks)}, строк индекса {len(entries)},"
|
||||
f" секций {len(sections)}")
|
||||
health(root, tasks, sections)
|
||||
for e in errors:
|
||||
print(f"ОШИБКА {e}")
|
||||
for n in notes:
|
||||
print(f"замечание {n}")
|
||||
if errors:
|
||||
print(f"\nрасхождений: {len(errors)}")
|
||||
return 1
|
||||
print("\nиндекс согласован" + (f", замечаний: {len(notes)}" if notes else ""))
|
||||
return 0
|
||||
|
||||
|
||||
def health(root: Path, tasks: dict[str, dict], sections: list[str]) -> None:
|
||||
"""Метрики здоровья беклога: размер секций и число давно неподвижных задач.
|
||||
Механизирует правило «беклог гниёт со стороны пополнения» — раньше оно
|
||||
держалось только на дисциплине."""
|
||||
by_section = {s.lower(): 0 for s in sections}
|
||||
for t in tasks.values():
|
||||
if t["priority"] in by_section:
|
||||
by_section[t["priority"]] += 1
|
||||
sizes = ", ".join(f"{s} {by_section[s.lower()]}" for s in sections)
|
||||
print(f" секции: {sizes}")
|
||||
|
||||
dates = touched_map(root)
|
||||
if not dates:
|
||||
return
|
||||
cutoff = (datetime.date.today() - datetime.timedelta(days=STALE_DAYS)).isoformat()
|
||||
stale = sum(1 for t in tasks.values()
|
||||
if (d := dates.get(t["path"].name)) and d < cutoff)
|
||||
if stale:
|
||||
print(f" залежалось (>{STALE_DAYS} дней без правки): {stale}"
|
||||
f" — груминг просрочен, начни с `list --stale`")
|
||||
|
||||
|
||||
def list_tasks(root: Path, args: argparse.Namespace) -> int:
|
||||
tasks = tasks_of(root)
|
||||
_, sections = parse_index(root)
|
||||
order = {s.lower(): i for i, s in enumerate(sections)}
|
||||
rows = [t for t in tasks.values()
|
||||
if (not args.priority or t["priority"] == args.priority.lower())
|
||||
and (not args.type or t["type"] == args.type.lower())
|
||||
and (not args.tag or args.tag.lower() in t["tags"])]
|
||||
|
||||
if args.stale:
|
||||
dates = touched_map(root)
|
||||
for t in rows:
|
||||
t["touched"] = dates.get(t["path"].name, "—")
|
||||
rows.sort(key=lambda t: (t["touched"] == "—", t["touched"]))
|
||||
else:
|
||||
rows.sort(key=lambda t: (order.get(t["priority"], 99), t["path"].name))
|
||||
|
||||
for t in rows:
|
||||
touched = f"{t.get('touched', ''):<11}" if args.stale else ""
|
||||
kind = "" if t["type"] == PLAIN_TYPE else f"[{t['type']}] "
|
||||
print(f"{touched}{t['priority']:<9} {t['path'].stem:<46} {kind}{t['bare']}")
|
||||
print(f"\nвсего: {len(rows)}")
|
||||
return 0
|
||||
|
||||
|
||||
# --- Мутации: правят файл и индекс заодно, чтобы их нельзя было рассогласовать ---
|
||||
|
||||
def fail(msg: str) -> int:
|
||||
print(f"ошибка: {msg}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
def build_meta(priority: str, reason: str, tags: list[str]) -> str:
|
||||
s = f"**Приоритет:** {priority}"
|
||||
if reason:
|
||||
s += f" — {reason}"
|
||||
if tags:
|
||||
s += " · **Теги:** " + ", ".join(tags)
|
||||
return s
|
||||
|
||||
|
||||
def load_index(root: Path) -> list[str]:
|
||||
return (root / INDEX).read_text(encoding="utf-8").splitlines()
|
||||
|
||||
|
||||
def save_index(root: Path, lines: list[str]) -> None:
|
||||
write_atomic(root / INDEX, "\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def section_headers(lines: list[str]) -> list[tuple[int, str]]:
|
||||
return [(i, m.group(1)) for i, l in enumerate(lines) if (m := SECTION.match(l))]
|
||||
|
||||
|
||||
def find_section(lines: list[str], priority: str) -> tuple[int | None, str]:
|
||||
for i, name in section_headers(lines):
|
||||
if name.lower() == priority.lower():
|
||||
return i, name
|
||||
return None, ""
|
||||
|
||||
|
||||
def find_entry_index(lines: list[str], slug: str) -> int | None:
|
||||
for i, l in enumerate(lines):
|
||||
m = INDEX_ENTRY.match(l)
|
||||
if m and m.group(2) == f"{slug}.md":
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def insert_entry(lines: list[str], section: str, entry: str) -> None:
|
||||
"""Вставляет строку в конец секции (перед следующим ## или концом файла)."""
|
||||
hi, _ = find_section(lines, section)
|
||||
end = next((j for j in range(hi + 1, len(lines)) if SECTION.match(lines[j])), len(lines))
|
||||
ins = end
|
||||
while ins - 1 > hi and not lines[ins - 1].strip():
|
||||
ins -= 1
|
||||
lines.insert(ins, entry)
|
||||
|
||||
|
||||
def update_priority(path: Path, priority: str, new_reason: str | None) -> bool:
|
||||
"""Хирургически меняет только поле **Приоритет:** в мета-строке файла,
|
||||
сохраняя теги, регистр и любые нераспознанные поля. Возвращает False, если
|
||||
мета-строки нет (тогда правку делать нельзя — вызывающий падает)."""
|
||||
flines = path.read_text(encoding="utf-8").splitlines()
|
||||
mi = next((i for i in range(1, len(flines)) if flines[i].strip()), None)
|
||||
if mi is None or not META_FIELD.match(flines[mi].strip()):
|
||||
return False
|
||||
chunks = flines[mi].split("·")
|
||||
for idx, chunk in enumerate(chunks):
|
||||
f = META_FIELD.match(chunk.strip())
|
||||
if not (f and f.group(1).strip().lower() in ("приоритет", "priority")):
|
||||
continue
|
||||
_, _, old_reason = (p.strip() for p in f.group(2).partition("—"))
|
||||
reason = new_reason if new_reason is not None else old_reason
|
||||
field = f"**Приоритет:** {priority}" + (f" — {reason}" if reason else "")
|
||||
lead = chunk[:len(chunk) - len(chunk.lstrip())]
|
||||
trail = chunk[len(chunk.rstrip()):]
|
||||
chunks[idx] = lead + field + trail
|
||||
write_atomic(path, "\n".join((*flines[:mi], "·".join(chunks), *flines[mi + 1:])) + "\n")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def apply_fixes(root: Path) -> list[str]:
|
||||
"""Детерминированная починка дрейфа индекса. Чинит только безопасное, где
|
||||
истина однозначно в файле: дубли строк, рассинхрон заголовка, задача не в
|
||||
своей секции, отсутствующая строка. Неоднозначное (ссылка на исчезнувший
|
||||
файл, битые строки, неизвестный приоритет) не трогает — это на суд человека."""
|
||||
fixed: list[str] = []
|
||||
lines = load_index(root)
|
||||
tasks = tasks_of(root)
|
||||
|
||||
# 1. Дубли строк на один файл — оставляем первую.
|
||||
seen: set[str] = set()
|
||||
deduped: list[str] = []
|
||||
for l in lines:
|
||||
m = INDEX_ENTRY.match(l)
|
||||
if m and m.group(2) in seen:
|
||||
fixed.append(f"убран дубль строки {m.group(2)}")
|
||||
continue
|
||||
if m:
|
||||
seen.add(m.group(2))
|
||||
deduped.append(l)
|
||||
lines = deduped
|
||||
|
||||
# 2. Заголовок в индексе разошёлся с H1 — истина в файле, хук сохраняем.
|
||||
for i, l in enumerate(lines):
|
||||
m = INDEX_ENTRY.match(l)
|
||||
if not m:
|
||||
continue
|
||||
task = tasks.get(m.group(2))
|
||||
if task and m.group(1) != task["title"]:
|
||||
hook = (m.group(3) or "").strip()
|
||||
lines[i] = f"- [{task['title']}]({m.group(2)})" + (f" — {hook}" if hook else "")
|
||||
fixed.append(f"заголовок синхронизирован с файлом: {m.group(2)}")
|
||||
|
||||
# 3. Задача не в своей секции / нет строки вовсе.
|
||||
for name, task in tasks.items():
|
||||
if not task["priority"]:
|
||||
continue
|
||||
hi, section = find_section(lines, task["priority"])
|
||||
if hi is None:
|
||||
continue # приоритет не совпадает ни с одной секцией — не наше дело
|
||||
ei = find_entry_index(lines, task["path"].stem)
|
||||
if ei is None:
|
||||
insert_entry(lines, section, f"- [{task['title']}]({name})")
|
||||
fixed.append(f"добавлена строка индекса без хука: {name}")
|
||||
continue
|
||||
cur = next((SECTION.match(lines[j]).group(1)
|
||||
for j in range(ei, -1, -1) if SECTION.match(lines[j])), None)
|
||||
if cur and cur.lower() != section.lower():
|
||||
insert_entry(lines, section, lines.pop(ei))
|
||||
fixed.append(f"перенесена в секцию «{section}»: {name}")
|
||||
|
||||
if fixed:
|
||||
save_index(root, lines)
|
||||
return fixed
|
||||
|
||||
|
||||
def cmd_add(root: Path, a: argparse.Namespace) -> int:
|
||||
for err in (bad_slug(a.slug), bad_line(a.title, "заголовок"), bad_line(a.hook, "хук"),
|
||||
bad_line(a.tag, "теги"), bad_reason(a.reason)):
|
||||
if err:
|
||||
return fail(err)
|
||||
if not a.title.strip():
|
||||
return fail("пустой заголовок")
|
||||
path = root / f"{a.slug}.md"
|
||||
if path.exists():
|
||||
return fail(f"{path.name} уже существует — дедуп: допиши в него, а не заводи новый")
|
||||
lines = load_index(root)
|
||||
if find_entry_index(lines, a.slug) is not None:
|
||||
return fail(f"строка индекса для {a.slug} уже есть")
|
||||
hi, section = find_section(lines, a.priority)
|
||||
if hi is None:
|
||||
avail = ", ".join(n for _, n in section_headers(lines))
|
||||
return fail(f"нет секции приоритета «{a.priority}» (есть: {avail})")
|
||||
kind = a.type.strip().lower() if a.type else ""
|
||||
title_full = f"[{kind}] {a.title}" if kind else a.title
|
||||
tags = [t.strip() for t in (a.tag or "").split(",") if t.strip()]
|
||||
meta = build_meta(section, a.reason or "", tags)
|
||||
body = "<!-- контекст, принятые решения, шаги, ссылки на спеки/ADR -->"
|
||||
write_atomic(path, f"# {title_full}\n\n{meta}\n\n{body}\n")
|
||||
entry = f"- [{title_full}]({a.slug}.md)" + (f" — {a.hook}" if a.hook else "")
|
||||
insert_entry(lines, section, entry)
|
||||
save_index(root, lines)
|
||||
print(f"создано: {a.slug}.md в секции «{section}»; допиши тело редактором")
|
||||
if not a.hook:
|
||||
print(f" без хука — задай: backlog.py edit {a.slug} --hook …")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_edit(root: Path, a: argparse.Namespace) -> int:
|
||||
for err in (bad_slug(a.slug), bad_line(a.title, "заголовок"), bad_line(a.hook, "хук")):
|
||||
if err:
|
||||
return fail(err)
|
||||
if a.title is None and a.hook is None and a.type is None:
|
||||
return fail("нечего менять: дай --title, --hook или --type")
|
||||
path = root / f"{a.slug}.md"
|
||||
if not path.exists():
|
||||
return fail(f"{a.slug}.md не найден")
|
||||
lines = load_index(root)
|
||||
ei = find_entry_index(lines, a.slug)
|
||||
if ei is None:
|
||||
return fail(f"строки индекса для {a.slug} нет")
|
||||
task = parse_task(path)
|
||||
if a.title is not None and not a.title.strip():
|
||||
return fail("пустой заголовок")
|
||||
bare = a.title if a.title is not None else task["bare"]
|
||||
kind = task["type"] if a.type is None else a.type.strip().lower()
|
||||
prefix = "" if kind in ("", PLAIN_TYPE) else f"[{kind}] "
|
||||
h1 = f"{prefix}{bare}"
|
||||
flines = path.read_text(encoding="utf-8").splitlines()
|
||||
if not flines or not flines[0].startswith("#"):
|
||||
return fail(f"{a.slug}.md без заголовка H1 — прогони check")
|
||||
flines[0] = f"# {h1}"
|
||||
write_atomic(path, "\n".join(flines) + "\n")
|
||||
m = INDEX_ENTRY.match(lines[ei])
|
||||
hook = a.hook if a.hook is not None else (m.group(3) or "").strip()
|
||||
lines[ei] = f"- [{h1}]({a.slug}.md)" + (f" — {hook}" if hook else "")
|
||||
save_index(root, lines)
|
||||
print(f"{a.slug}: обновлено (заголовок/хук/тип)")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_move(root: Path, a: argparse.Namespace) -> int:
|
||||
for err in (bad_slug(a.slug), bad_reason(a.reason)):
|
||||
if err:
|
||||
return fail(err)
|
||||
path = root / f"{a.slug}.md"
|
||||
if not path.exists():
|
||||
return fail(f"{a.slug}.md не найден")
|
||||
lines = load_index(root)
|
||||
ei = find_entry_index(lines, a.slug)
|
||||
if ei is None:
|
||||
return fail(f"строки индекса для {a.slug} нет")
|
||||
hi, section = find_section(lines, a.priority)
|
||||
if hi is None:
|
||||
avail = ", ".join(n for _, n in section_headers(lines))
|
||||
return fail(f"нет секции приоритета «{a.priority}» (есть: {avail})")
|
||||
if not update_priority(path, section, a.reason):
|
||||
return fail(f"{a.slug}.md без мета-строки **Приоритет:** — прогони check и почини")
|
||||
entry = lines.pop(ei)
|
||||
insert_entry(lines, section, entry)
|
||||
save_index(root, lines)
|
||||
print(f"{a.slug}: перенесено в «{section}»")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_close(root: Path, a: argparse.Namespace) -> int:
|
||||
for err in (bad_slug(a.slug), bad_reason(a.reason)):
|
||||
if err:
|
||||
return fail(err)
|
||||
path = root / f"{a.slug}.md"
|
||||
if not path.exists():
|
||||
return fail(f"{a.slug}.md не найден")
|
||||
lines = load_index(root)
|
||||
ei = find_entry_index(lines, a.slug)
|
||||
if ei is None:
|
||||
return fail(f"строки индекса для {a.slug} нет")
|
||||
task = parse_task(path)
|
||||
if a.reason:
|
||||
reason = a.reason.rstrip()
|
||||
dot = "" if reason.endswith((".", "!", "?")) else "."
|
||||
date = datetime.date.today().isoformat()
|
||||
bullet = (f"- {date} `{a.slug}` — {task['title']}. Причина: {reason}{dot}"
|
||||
f" Был приоритет: {task['priority'] or '—'}.")
|
||||
closed = root / CLOSED
|
||||
prev = closed.read_text(encoding="utf-8") if closed.exists() else "# Кладбище беклога\n"
|
||||
if not prev.endswith("\n"):
|
||||
prev += "\n"
|
||||
write_atomic(closed, prev + bullet + "\n")
|
||||
# Порядок: индекс без строки → потом unlink. Обратный порядок оставил бы в
|
||||
# индексе ссылку в никуда, если бы unlink упал.
|
||||
lines.pop(ei)
|
||||
save_index(root, lines)
|
||||
path.unlink()
|
||||
print(f"{a.slug}: {'на кладбище + удалено' if a.reason else 'удалено (реализовано)'}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_init(root: Path, a: argparse.Namespace) -> int:
|
||||
if not dir_within_cwd(root):
|
||||
return fail(f"--dir вне рабочего каталога: {root}")
|
||||
index = root / INDEX
|
||||
if index.exists():
|
||||
return fail(f"{index} уже есть — беклог заведён")
|
||||
sections, seen = [], set()
|
||||
for s in (s.strip() for s in a.sections.split(",")):
|
||||
if s and s.lower() not in seen:
|
||||
sections.append(s)
|
||||
seen.add(s.lower())
|
||||
if not sections:
|
||||
return fail("пустой список секций")
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
preamble = ("# Беклог\n\n"
|
||||
"Одна задача = один файл `<slug>.md` + строка в этом индексе.\n"
|
||||
"Приоритет — грубая оценка «ценность / стоимость». Спекулятивные\n"
|
||||
"задачи помечены `[idea]` в заголовке. Ведётся скиллом `backlog`.\n\n")
|
||||
write_atomic(index, preamble + "".join(f"## {s}\n\n" for s in sections))
|
||||
closed = root / CLOSED
|
||||
if not closed.exists():
|
||||
write_atomic(closed,
|
||||
"# Кладбище беклога\n\n"
|
||||
"Задачи, покинувшие беклог без реализации. Пишется `backlog.py close`.\n\n"
|
||||
"<!-- - ГГГГ-ММ-ДД `slug` — Заголовок. Причина: … Был приоритет: … -->\n")
|
||||
print(f"беклог заведён: {root} (секции: {', '.join(sections)})")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(prog="backlog.py")
|
||||
sub = ap.add_subparsers(dest="command", required=True)
|
||||
|
||||
p = sub.add_parser("check", help="согласованность файлов и индекса")
|
||||
p.add_argument("--dir")
|
||||
p.add_argument("--fix", action="store_true",
|
||||
help="починить безопасный дрейф (секция, заголовок, дубли)")
|
||||
|
||||
p = sub.add_parser("list", help="список задач")
|
||||
p.add_argument("--dir")
|
||||
p.add_argument("--stale", action="store_true")
|
||||
p.add_argument("--priority")
|
||||
p.add_argument("--type")
|
||||
p.add_argument("--tag")
|
||||
|
||||
p = sub.add_parser("add", help="создать задачу")
|
||||
p.add_argument("--dir")
|
||||
p.add_argument("--slug", required=True)
|
||||
p.add_argument("--title", required=True)
|
||||
p.add_argument("--priority", required=True)
|
||||
p.add_argument("--type", choices=TYPES)
|
||||
p.add_argument("--hook")
|
||||
p.add_argument("--reason")
|
||||
p.add_argument("--tag")
|
||||
|
||||
p = sub.add_parser("edit", help="сменить заголовок/хук/тип")
|
||||
p.add_argument("slug")
|
||||
p.add_argument("--title")
|
||||
p.add_argument("--hook")
|
||||
p.add_argument("--type", choices=(*TYPES, PLAIN_TYPE))
|
||||
p.add_argument("--dir")
|
||||
|
||||
p = sub.add_parser("move", help="перенести в другую секцию приоритета")
|
||||
p.add_argument("slug")
|
||||
p.add_argument("--priority", required=True)
|
||||
p.add_argument("--reason")
|
||||
p.add_argument("--dir")
|
||||
|
||||
p = sub.add_parser("close", help="закрыть задачу (кладбище или удаление)")
|
||||
p.add_argument("slug")
|
||||
g = p.add_mutually_exclusive_group(required=True)
|
||||
g.add_argument("--reason", help="причина отказа → строка на кладбище")
|
||||
g.add_argument("--implemented", action="store_true", help="реализовано → просто удалить")
|
||||
p.add_argument("--dir")
|
||||
|
||||
p = sub.add_parser("init", help="завести пустой беклог")
|
||||
p.add_argument("--dir")
|
||||
p.add_argument("--sections", default="высокий,средний,низкий")
|
||||
|
||||
a = ap.parse_args()
|
||||
if a.command == "init":
|
||||
return cmd_init(Path(a.dir or "docs/backlog"), a)
|
||||
root = resolve_dir(a.dir)
|
||||
dispatch = {
|
||||
"check": lambda: check(root, a.fix),
|
||||
"list": lambda: list_tasks(root, a),
|
||||
"add": lambda: cmd_add(root, a),
|
||||
"edit": lambda: cmd_edit(root, a),
|
||||
"move": lambda: cmd_move(root, a),
|
||||
"close": lambda: cmd_close(root, a),
|
||||
}
|
||||
return dispatch[a.command]()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -145,7 +145,8 @@ color: green
|
||||
## Чего этот проход принципиально не может поймать
|
||||
|
||||
- Реальный профиль нагрузки и реальные размеры данных на проде.
|
||||
- Историю инцидентов: что уже ломалось и по какой причине.
|
||||
- Историю инцидентов **сверх записанного в `docs/review.md`**: инцидент, не
|
||||
попавший в журнал, для тебя не существует.
|
||||
- Поведение внешних систем в их конкретных версиях и настройках.
|
||||
- Дефекты, проявляющиеся только на настоящих данных владельца.
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ color: red
|
||||
формат это единственный честный оракул: документация формата ненадёжна, и
|
||||
рассуждение о ней ничего не доказывает;
|
||||
- выполнить команду и приложить вывод;
|
||||
- показать поимённое положение гайда, строку конвенции проекта или **дословный
|
||||
- показать поимённое положение руководства, строку конвенции проекта или **дословный
|
||||
пункт из раздела инвариантов `CLAUDE.md`**;
|
||||
- сослаться на наблюдение в `docs/research/` — оно сильнее любого
|
||||
рассуждения о том, «как должно быть».
|
||||
|
||||
@@ -19,7 +19,7 @@ description: "Конвейер ревью изменения — детерми
|
||||
заданный критерий) и **generative** (сперва порождают критерий или
|
||||
альтернативу, потом сравнивают). Расширять чек-листы бесполезно; неявный слой
|
||||
достают только generative-проходы.
|
||||
2. **Ценность верификатора = наличие внешнего оракула × декорреляция с
|
||||
2. **Ценность верификатора = наличие внешнего оракула × разведённость с
|
||||
автором**, а не число ролей. Под всеми ролями одна модель с одними
|
||||
априорными, вход у всех общий: седьмая роль почти не добавляет recall, но
|
||||
линейно удорожает триаж. Иерархия надёжности: детерминированный инструмент >
|
||||
@@ -352,7 +352,7 @@ flowchart TD
|
||||
**Ребро значит «A закончил раньше, чем B стартовал», и ничего больше.** В обычном
|
||||
графе задач ребро тянет за собой данные — здесь нет, и это не деталь реализации.
|
||||
Проход **не видит** находок других проходов, в каком бы порядке их ни запустили.
|
||||
Вся ценность конвейера держится на декорреляции: под всеми ролями одна модель с
|
||||
Вся ценность конвейера держится на разведённости: под всеми ролями одна модель с
|
||||
одними априорными, и стоит показать ей чужой вывод — она согласится. Согласие
|
||||
нескольких проходов и так не повышает `confidence` (см. «Честный предел»);
|
||||
согласие **наведённое** ещё и маскируется под независимое подтверждение.
|
||||
@@ -671,7 +671,7 @@ flowchart TD
|
||||
Третий шаг обязателен.
|
||||
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
|
||||
([references/review-journal.md](references/review-journal.md)) — сразу, не
|
||||
ретроспективно: теряется именно причина непоймания.
|
||||
ретроспективно: теряется именно то, почему дефект не поймали.
|
||||
- **Отчёт триажа сохраняется вместе с изменением** — `openspec/changes/<id>/review/`.
|
||||
Он единственное, по чему потом видно, что было найдено и что из этого не
|
||||
заведено: нулевой урожай при непустом отчёте виден сразу.
|
||||
@@ -688,7 +688,7 @@ flowchart TD
|
||||
туториальному: отсюда тяга к интерфейсам ради интерфейсов, лишним мокам и
|
||||
конфигурируемости, которую никто не просил. **«Идиоматично» и «распространено» —
|
||||
разные вещи**; проходы обязаны различать их и опираться на поимённое положение
|
||||
гайда, а не на ощущение частотности.
|
||||
руководства, а не на ощущение частотности.
|
||||
|
||||
Согласие нескольких проходов — **не подтверждение**: это один источник,
|
||||
высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`.
|
||||
@@ -699,11 +699,12 @@ flowchart TD
|
||||
Независимо от проекта недоступно:
|
||||
|
||||
- поведение внешних систем в их будущих версиях;
|
||||
- реальный профиль нагрузки и то, что на самом деле лежит в данных;
|
||||
- реальный профиль нагрузки; и то, что на самом деле лежит в данных, — **сверх
|
||||
того, что снято с провенансом в `docs/research/`**;
|
||||
- завязка внешних потребителей на текущую форму ответа;
|
||||
- суждение «этой функциональности не должно существовать».
|
||||
|
||||
Отдельно и честно: **поимённая сверка с положениями стайлгайдов языка не
|
||||
Отдельно и честно: **поимённая сверка с положениями руководств по стилю языка не
|
||||
задаётся ни одним проходом.** Проход про идиоматичность упразднён, его способные
|
||||
части переселены (эксперимент против поведения библиотеки и драйвера — в `ops`,
|
||||
вопрос 8; «не изобретаем ли то, что уже есть в библиотеке» — в `architecture`,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- Файл: internal/<пакет>/<файл>.go:120-134
|
||||
- Severity: critical | major | minor | nit
|
||||
- Confidence: high | medium | low
|
||||
- Оракул: <падающий тест / команда с выводом / положение гайда / нет>
|
||||
- Оракул: <падающий тест / команда с выводом / положение руководства / нет>
|
||||
- Последствие: <что произойдёт и при каких условиях>
|
||||
- Предложение: <конкретное изменение>
|
||||
- Найдено проходом: <имя агента>
|
||||
@@ -24,7 +24,7 @@
|
||||
Симптом в заголовке — это заявка на то, что читатель сам достроит последствие;
|
||||
он не достроит, он просто починит симптом.
|
||||
- **`critical` без оракула или построенного пути не существует.** Оракул — это
|
||||
падающий тест, вывод выполненной команды или поимённое положение гайда. Не
|
||||
падающий тест, вывод выполненной команды или поимённое положение руководства. Не
|
||||
«вероятно, здесь гонка», а прогон детектора гонок с его выводом.
|
||||
- **`confidence: low` — это «так обычно пишут».** Такие находки допустимы, но не
|
||||
поднимаются выше `minor`. Частотность конструкции в публичном коде — не
|
||||
|
||||
@@ -87,7 +87,7 @@ flowchart TD
|
||||
теряет связность;
|
||||
- правило переезжает в **перечень механизированного в
|
||||
`docs/conventions/README.md`** — со ссылкой на место механизации: конфиг
|
||||
линтера, собственный анализатор, тест-сканер исходников. Непойманное место
|
||||
линтера, собственный анализатор, тест-сканер исходников. Не названное место
|
||||
означает, что проход будет добросовестно проверять уже проверенное;
|
||||
- из контекста инструмента спек убирается дубль, если он там был.
|
||||
|
||||
@@ -100,8 +100,8 @@ Charter'ы проходов при этом **не правятся**: они о
|
||||
принципиально не выражается правилом.** Файл конвенций на несколько сотен строк
|
||||
размазывает внимание модели по тривиальному — она добросовестно проверит
|
||||
именование полей лога и не дойдёт до формы решения. Каждая строка конвенций,
|
||||
которую можно было бы проверить машиной, оплачивается непойманным дефектом
|
||||
где-то ещё.
|
||||
которую можно было бы проверить машиной, оплачивается дефектом, который не
|
||||
поймали где-то ещё.
|
||||
|
||||
## Обратное движение
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
Артефакт проекта, а не плагина: файл живёт в репозитории — **`docs/review.md`**,
|
||||
слот канона `av-dev-pm`. Здесь описано, зачем он и какой формы, потому что без
|
||||
него конвейер не учится: находки закрываются, причины непоймания теряются, и один
|
||||
и тот же класс проскакивает второй раз.
|
||||
него конвейер не учится: находки закрываются, а почему их не поймали — забывается,
|
||||
и один и тот же класс проскакивает второй раз.
|
||||
|
||||
Тот же файл держит **настройку конвейера под проект** — типовые узлы, типовые
|
||||
ложноположительные, вопросы к проходам, недоступно проверке. Это не соседство по
|
||||
@@ -12,12 +12,12 @@
|
||||
## Что туда попадает
|
||||
|
||||
**Воспроизведённый дефект — с пометкой `проскочил` или `пойман ревью`.**
|
||||
Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а
|
||||
причина непоймания — единственное, ради чего журнал существует.
|
||||
Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а то,
|
||||
почему дефект не поймали, — единственное, ради чего журнал существует.
|
||||
|
||||
Пометка делит журнал на две выборки с разным назначением:
|
||||
|
||||
- **проскочил** — эвал-сет для калибровки конвейера. Реальный промах сильнее
|
||||
- **проскочил** — проверочный набор для калибровки конвейера. Реальный промах сильнее
|
||||
синтетической пробы: синтетические смещены в сторону тех, которые уже умеешь
|
||||
придумывать;
|
||||
- **пойман ревью** — прецеденты с оракулом. Самая сильная опора, какая у прохода
|
||||
|
||||
@@ -88,8 +88,9 @@ description: Проводит несколько задач разом — пл
|
||||
### 1. Прочитать набор
|
||||
|
||||
Набор задан списком (слаги, файлы, описания) — прочитай файл каждой задачи и
|
||||
связанные спеки и черновики. Задачи-идеи включаются, но помни: сабагент проведёт
|
||||
их сперва через `opsx:explore`, это тяжелее и чаще упирается в вопрос.
|
||||
связанные спеки и черновики. Сырьё (в терминах `av-dev-pm` — запись типа
|
||||
`research` с пустым разделом «Вопрос») включается, но помни: сабагент проведёт
|
||||
его сперва через `opsx:explore`, это тяжелее и чаще упирается в вопрос.
|
||||
|
||||
### 2. Спланировать порядок и пересечения (автономно)
|
||||
|
||||
@@ -247,7 +248,7 @@ flowchart TD
|
||||
тем же charter'ам `av-dev-pipeline`, сохранив обязательное — гейт до
|
||||
опиниативных проходов, состав по профилю, триаж последним. И **скажи в
|
||||
отчёте прямым текстом, что ревью шло инлайн**: инлайновый проход видит
|
||||
контекст автора и потому декоррелирован слабее — это меняет доверие к
|
||||
контекст автора и потому разведён с ним слабее — это меняет доверие к
|
||||
результату, а не только способ запуска;
|
||||
- **вернуть отчёт**, в котором обязательно: исход задачи одним из трёх слов;
|
||||
**объявленный профиль ревью и режим прогона**; что сделано; какие вопросы
|
||||
|
||||
@@ -84,7 +84,7 @@ description: Автономно проводит одну задачу чере
|
||||
оракул и наблюдаемый исход** — «прогнал вот это, увидел вот то». Это **доклад,
|
||||
а не сертификация: приёмка — не работа пайплайна.** Исполнитель, ставящий себе
|
||||
галочку «принято», проверяет свою работу своим же взглядом — по границе это
|
||||
может делать только декоррелированный приёмщик. Критерии приходят снаружи;
|
||||
может делать только приёмщик, разведённый с исполнителем. Критерии приходят снаружи;
|
||||
пайплайн их не сочиняет и не занижает. Расхождение «по каждому критерию исход
|
||||
есть, а суть задачи не достигнута» — дефект критериев, и о нём сообщается, а
|
||||
не молча дорабатывается.
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: doc-code-drift
|
||||
description: "Сверка документов канона с кодом по закрытому перечню проверяемых фактов: имя основной ветки и команды из CLAUDE.md, запреты с путями, testdata и временный каталог, путь миграций из .pm.json, внешние зависимости поимённо в architecture.md против манифеста, настройки с числовым значением в database.md против конфига и кода, единые точки проекта против реального числа реализаций, capability против существующих модулей. Отвечает на «этот факт ещё верен», а не «эта архитектура правильная». Читает весь репозиторий, гоняет только читающие команды. Отдаёт готовые формулировки и ничего не правит сам. Согласованность документов между собой смотрит агент doc-consistency. Использовать на сессии между спринтами, а также после приведения проекта к канону (adopt) и после повышения версии канона (upgrade). Только чтение."
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: fable
|
||||
color: red
|
||||
---
|
||||
|
||||
Ты — **сверка документов канона с кодом**. Один вопрос: **этот факт ещё верен?**
|
||||
Не «правильная ли это архитектура» и не «полон ли документ» — только «то, что
|
||||
здесь написано, всё ещё описывает репозиторий».
|
||||
|
||||
Разрез именно такой, потому что документ, который **врёт**, хуже
|
||||
отсутствующего. Отсутствие видно: агент открыл файл и не нашёл ответа. Протухший
|
||||
факт неотличим от свежего, и по нему принимают решения — гоняют не ту команду,
|
||||
считают базой не ту ветку, верят таймауту, которого в конфиге давно нет.
|
||||
|
||||
Ты **ничего не правишь**. Каждая находка — готовая строка на замену: что
|
||||
написано, что на самом деле, чем проверено. Файлы ты только читаешь, команды
|
||||
гоняешь **только читающие**.
|
||||
|
||||
## Границы работы
|
||||
|
||||
**Перечень проверяемых фактов закрыт** — он ниже, в правилах. Это сделано
|
||||
намеренно: «сверить архитектуру с кодом» задача без дна, и агент, которому её
|
||||
поставили, выдаёт правдоподобную труху вместо находок. Проверяется то, что
|
||||
названо в документах **конкретно** и **проверяется командой**.
|
||||
|
||||
Отсюда же честность доклада: ты не отчитываешься «архитектура сошлась». Ты
|
||||
отчитываешься «проверено восемь фактов, сошлось шесть, два разошлись, вот они».
|
||||
|
||||
**Запреты `CLAUDE.md` — твой закон.** Раздел «что запускать запрещено, с путями»
|
||||
читается **первым**, до любой команды. Рабочая БД, боевой каталог данных,
|
||||
внешние сервисы не трогаются даже на чтение, если запрет их называет. Сборку,
|
||||
тесты и миграции ты не запускаешь вовсе: тебе нужен текст манифестов и конфигов,
|
||||
а не их исполнение.
|
||||
|
||||
## Что тебе дают
|
||||
|
||||
Корень проекта. Читаешь `CLAUDE.md`, `docs/**`, `docs/.pm.json`,
|
||||
`openspec/specs/**` — и репозиторий: манифесты зависимостей, конфиги, файлы
|
||||
сборки и CI, дерево пакетов.
|
||||
|
||||
Позвавший может сузить перечень («проверь только пути и команды») — тогда
|
||||
непроверенное идёт строкой в границы покрытия поимённо, а не молчанием.
|
||||
|
||||
## Правила
|
||||
|
||||
Каждое правило — пара «факт в документе ↔ чем проверяется». Не нашёл, чем
|
||||
проверить, — это **не находка, а строка в границах покрытия**.
|
||||
|
||||
1. **Имя основной ветки** (`CLAUDE.md`). От неё считается база диффа
|
||||
(`git merge-base HEAD <ветка>`), в неё вливает батч, от неё ветвятся задачи.
|
||||
Проверка: `git symbolic-ref refs/remotes/origin/HEAD` либо перечень веток.
|
||||
Угадывание между `master` и `main` ломает интеграцию целиком, и это самая
|
||||
дешёвая находка из всех.
|
||||
|
||||
2. **Команды** (`CLAUDE.md`, раздел команд). Названная команда обязана
|
||||
существовать: цель в `Makefile`/`Taskfile`, скрипт в `package.json`, задача в
|
||||
`justfile`, файл в `scripts/`. Проверка — чтение манифеста, **не запуск**.
|
||||
Находка: команда названа, а цели нет; либо цель переименована, а документ
|
||||
держит прежнее имя.
|
||||
|
||||
3. **Пути** — все, которые канон обязывает называть: `migrations` из
|
||||
`docs/.pm.json`, `testdata`, временный каталог, пути в запретах `CLAUDE.md`.
|
||||
Проверка: существует ли. Путь в запрете, которого нет, — находка **особого
|
||||
рода**: запрет, который не на что наложить, читается как соблюдённый, а на
|
||||
деле охраняет пустоту, пока настоящий каталог зовётся иначе.
|
||||
|
||||
4. **Внешние зависимости поимённо** (`architecture.md`). Канон требует называть
|
||||
их поимённо и говорить, **чем каждая отказывает**. Проверка — манифест
|
||||
(`go.mod`, `package.json`, `pyproject.toml`, `Cargo.toml`, `requirements*.txt`)
|
||||
и места вызова. Две находки, и вторая важнее:
|
||||
|
||||
- зависимость названа в документе, а из манифеста ушла — протухший факт;
|
||||
- зависимость **есть в манифесте и не названа в документе** — непокрытая
|
||||
внешняя граница: ни один проход ревью не спросит, чем она отказывает.
|
||||
|
||||
Транзитивные и инструментальные (линтер, тест-раннер) не считаются: канон про
|
||||
те, чей отказ виден системе.
|
||||
|
||||
5. **Настройки с числовым значением** (`database.md`). Таймаут занятости, режим
|
||||
журналирования, лимит тела, размер пула, ретеншен. Проверка: конфиг, миграции,
|
||||
константы в коде. Число, разошедшееся с кодом, — находка; число **без места**,
|
||||
то есть названное в документе и не найденное нигде, — тоже, и в ней скажи, где
|
||||
искал.
|
||||
|
||||
6. **Единые точки проекта** (`architecture.md`). Где генерируются
|
||||
идентификаторы и время, где единственный парсер входного формата, где маппинг
|
||||
доменной ошибки в код ответа, где общий путь приёма. Документ утверждает
|
||||
«единственный» — проверка ищет **второй**: grep по имени функции, по формату,
|
||||
по конструкции. Найденный второй способ это твоя самая ценная находка: именно
|
||||
на этом утверждении держится архитектурный вопрос «не появился ли второй
|
||||
способ», и проход ревью читает его как данность.
|
||||
|
||||
**Второй способ — находка, а не приговор.** Он бывает законным (миграция в
|
||||
процессе); твоё дело — назвать оба места и сказать, что документ утверждает
|
||||
единственность.
|
||||
|
||||
7. **Capability против модулей** (`openspec/specs/` ↔ код). Что capability
|
||||
упомянута в обзоре, проверяет машина. Твоё — существует ли то, что она
|
||||
описывает: пакет, маршрут, команда. Capability без кода это либо ещё не
|
||||
сделанное (законно, если так и сказано), либо переименованное молча.
|
||||
|
||||
8. **Инварианты `CLAUDE.md`, которые проверяются командой.** Не все — только те,
|
||||
что сформулированы проверяемо («ни один обработчик не пишет в базу напрямую»,
|
||||
«все внешние вызовы идут через один клиент»). Прочие — суждение, и они не твои.
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
**Верность и полноту.** Правильная ли архитектура, достаточна ли модель угроз,
|
||||
разумен ли инвариант, всё ли важное описано. Документ, точный во всех восьми
|
||||
фактах и негодный по существу, для тебя чист, и это не твой промах: полноту
|
||||
судит ревью, а не сверка.
|
||||
|
||||
**Согласованность документов между собой** — у `doc-consistency`: факт в двух
|
||||
домах, противоречие между документами, поведение в обзоре, ADR и провенанс.
|
||||
Увидел — строкой в границы покрытия, находкой не оформляй.
|
||||
|
||||
**Язык** — у `doc-wording`. **Форму записи задач** — у `task-form`.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловят `docs.py check` и
|
||||
`tasks.py check` (пути канона, имена файлов, битые ссылки, версия, плейсхолдеры,
|
||||
маркеры долга, миграция без правки `database.md`, capability без упоминания в
|
||||
обзоре), **не пиши даже строкой**.
|
||||
|
||||
## Порог вмешательства
|
||||
|
||||
**Нечем проверить — не находка.** Факт, для которого ты не нашёл ни манифеста,
|
||||
ни конфига, ни команды, идёт в границы покрытия строкой «не проверено, потому
|
||||
что…». Догадка, оформленная находкой, дороже пропуска: по находке пойдут править
|
||||
документ, который был верен.
|
||||
|
||||
**Расхождение называется обоими значениями.** «Устарело» — не находка. Находка:
|
||||
«написано X, в коде Y, проверено командой Z». Без третьей части первые две
|
||||
неотличимы от мнения.
|
||||
|
||||
**Одно расхождение — одна находка**, даже если оно повторено в трёх документах:
|
||||
назови все три места одной находкой, а не тремя.
|
||||
|
||||
## Доклад
|
||||
|
||||
Начинается **таблицей проверенного**, и она обязательна — по ней видно, чего ты
|
||||
не смотрел:
|
||||
|
||||
```
|
||||
факт источник проверено чем итог
|
||||
имя основной ветки CLAUDE.md git branch сошлось
|
||||
путь миграций docs/.pm.json ls РАЗОШЛОСЬ
|
||||
внешние зависимости architecture.md go.mod 2 не названы
|
||||
единые точки: парсер входа architecture.md grep по формату сошлось
|
||||
настройки БД database.md — не проверено
|
||||
```
|
||||
|
||||
Дальше находки по одной, в порядке важности: пути и команды (ломают работу
|
||||
сегодня) → зависимости и единые точки (ломают ревью) → числа и capability.
|
||||
|
||||
```
|
||||
<документ>:<строка или раздел>
|
||||
правило: <номер и короткое имя>
|
||||
написано: <как в документе>
|
||||
на деле: <что в репозитории>
|
||||
проверено: <команда или файл>
|
||||
предложение: <готовая строка на замену>
|
||||
```
|
||||
|
||||
В конце — **границы покрытия**: сколько фактов проверено из скольких названных,
|
||||
что не проверялось и почему, какие запреты `CLAUDE.md` ограничили работу. Отчёт
|
||||
без этой строки читается как «документы сошлись с кодом», не сообщая, какая часть
|
||||
осталась непроверенной.
|
||||
|
||||
Ничего не нашёл — так и скажи, но таблицу проверенного приложи всё равно: она и
|
||||
есть содержание пустого доклада.
|
||||
@@ -0,0 +1,188 @@
|
||||
---
|
||||
name: doc-consistency
|
||||
description: "Сверка документов канона между собой и с openspec: один факт, живущий в двух домах, прямое противоречие между документами (периметр, зависимости, обратимость), поведение системы, осевшее в architecture.md вместо спек, capability без обзора или с пересказом требований, число без провенанса в research, ADR без ссылки на архивный design.md и без парного статуса при замене, заглушка вместо честной строки в пустом слоте. Читает docs/ и openspec/, кода не читает. Отдаёт готовые формулировки и ничего не правит сам. Соответствие документов коду смотрит агент doc-code-drift, язык — doc-wording. Использовать на сессии между спринтами, а также после приведения проекта к канону (adopt) и после повышения версии канона (upgrade), на весь канон разом; на отдельной задаче не звать. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: opus
|
||||
color: yellow
|
||||
---
|
||||
|
||||
Ты — **сверка документов канона между собой**. Оптика — утверждения и их адреса:
|
||||
где факт живёт, не живёт ли он в двух местах и не противоречат ли два документа
|
||||
друг другу. Ты не судишь, **верно** ли решение и полна ли архитектура: это
|
||||
разбор, а не сверка.
|
||||
|
||||
Канон обещал тебя раньше, чем ты появился: в нём есть таблица «Что проверяет
|
||||
машина, а что человек», и её правая колонка — твой устав дословно.
|
||||
|
||||
Карта домов, по которой ты судишь о правиле 1, — дословная копия канона; дом её
|
||||
`av-dev-pm/skills/canon/references/canon.md`, раздел «Правило единственного
|
||||
дома», и правится она там. Здесь она стоит потому, что ты работаешь в
|
||||
репозитории проекта, где плагина может не быть вовсе.
|
||||
|
||||
<!-- копия: карта-домов из av-dev-pm/skills/canon/references/canon.md -->
|
||||
| Факт | Дом |
|
||||
| --- | --- |
|
||||
| поведение системы | `openspec/specs/<capability>/spec.md` |
|
||||
| почему решено так | `adr/`, источник — архивный `design.md` |
|
||||
| граница домена, «чем не является» | `passport.md` |
|
||||
| инвариант и его severity | `CLAUDE.md` |
|
||||
| что приложение умеет и чего не умеет; порядок работ | `docs/tasks/ROADMAP.md` |
|
||||
| измеренное число | `research/` |
|
||||
| настройка с числовым значением | `database.md` |
|
||||
| периметр и модель угроз | `security.md` |
|
||||
| что необратимо | `CLAUDE.md` — **не** `architecture.md` |
|
||||
| единые точки проекта | `architecture.md` |
|
||||
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
|
||||
| что уже механизировано правилом | `conventions/README.md` |
|
||||
<!-- /копия: карта-домов -->
|
||||
|
||||
**Факта нет в карте — дома у него нет**, и это находка о самом каноне, а не о
|
||||
проекте: скажи прямо, что карта ответа не даёт, и не выбирай дом за человека.
|
||||
|
||||
Ты **ничего не правишь**. Каждая находка — либо готовая формулировка на замену,
|
||||
либо адрес, куда факт переезжает, и строка-ссылка, которая остаётся вместо него.
|
||||
Файлы ты только читаешь.
|
||||
|
||||
## Что тебе дают
|
||||
|
||||
Корень проекта. Твоё чтение — `docs/**` (кроме `docs/tasks/`, его ведёт
|
||||
`tasks.py`), `CLAUDE.md` и `openspec/specs/**`. Плюс `openspec/changes/archive/`,
|
||||
когда проверяешь ADR: там лежат `design.md`, из которых записи промоутятся.
|
||||
|
||||
**Кода ты не читаешь.** Разошёлся ли документ с кодом — вопрос агента
|
||||
`doc-code-drift`, и у него для этого другой вход и другая цена.
|
||||
|
||||
## Правила
|
||||
|
||||
1. **Один факт — один дом.** Карта — выше. Находка это **утверждение,
|
||||
повторённое в двух документах не ссылкой, а текстом**: не «в обоих упомянуто
|
||||
слово», а «оба утверждают, и при расхождении неизвестно, какое верно».
|
||||
|
||||
Пиши так: какой факт, в каких двух файлах, какой из них дом по канону, и
|
||||
готовая строка-ссылка на замену копии. Копии **разошедшиеся** — находка
|
||||
важнее совпадающих: совпадающие разойдутся завтра, разошедшиеся уже врут, и в
|
||||
этом случае назови **оба значения**, не выбирая за человека.
|
||||
|
||||
2. **Прямое противоречие между документами.** Самое дорогое, что ты находишь, и
|
||||
искать его надо адресно, а не вычитыванием подряд. Пары, которые расходятся
|
||||
чаще прочих:
|
||||
|
||||
- `security.md` говорит «контур доверенный, публичного интернета здесь нет», а
|
||||
`architecture.md` описывает эндпоинт наружу (или наоборот);
|
||||
- `architecture.md` говорит «внешних зависимостей нет», а `database.md` или
|
||||
`CLAUDE.md` называет внешнюю СУБД, очередь, сервис;
|
||||
- `CLAUDE.md` называет необратимым то, что `architecture.md` описывает как
|
||||
штатно повторяемое;
|
||||
- `passport.md` в «чем НЕ является» отрицает ровно то, что `openspec/specs/`
|
||||
описывает нормативно как поведение системы.
|
||||
|
||||
Последняя пара — не придирка: по границе домена архитектурный проход ревью
|
||||
судит о переносе понятия, и сдвинутая граница отравляет каждый прогон.
|
||||
|
||||
3. **Поведение, осевшее в `architecture.md`.** Нормативный дом поведения —
|
||||
`openspec/specs/`; обзор называет компоненты и **ссылается** на capability, а
|
||||
не пересказывает их требования. Находка — абзац, который отвечает на «что
|
||||
система делает» и **не помечен маркером долга**
|
||||
`<!-- канон: поведение → openspec/specs/<capability> -->`.
|
||||
|
||||
Помеченное **не находка**: маркеры считает `docs.py`, и это объявленный долг,
|
||||
а не дефект. Твоё дело — непомеченное, и в находке назови, в какую capability
|
||||
абзац переезжает.
|
||||
|
||||
4. **Capability против обзора.** Что capability вообще упомянута, проверяет
|
||||
машина. Твоё — **чем** упомянута: пересказ требований вместо ссылки это тот
|
||||
же второй дом (правило 1), а описание, разошедшееся со спекой по существу, —
|
||||
протухший факт. Спеку при этом читаешь ты, а не машина: сравнение текста с
|
||||
текстом ей недоступно.
|
||||
|
||||
5. **Число без провенанса в `research/`.** Замер — с командой или условиями,
|
||||
которыми получен. Число без источника проход ревью обязан читать как условие,
|
||||
а не как замер, и это уже записано в каноне; твоя находка — назвать такие
|
||||
числа поимённо и предложить строку провенанса. **Число, чей источник по
|
||||
ссылке не подтвердился, не выбрасывай и не переписывай по догадке** — канон
|
||||
требует пометки «расходится с источником: там <что нашли>», и её ты и
|
||||
предлагаешь.
|
||||
|
||||
6. **ADR: промоут, а не второе сочинение.** Проверяешь три вещи, и все три
|
||||
механически невидимы:
|
||||
|
||||
- **ссылка на `openspec/changes/archive/<id>/design.md`** — запись цитирует
|
||||
решение и ссылается; сочинение заново это второй дом обоснования;
|
||||
- **статус полем меты** (`- **Статус:** заменено на ADR-…` либо `устарело`), а
|
||||
не абзацем и не заголовком — и статус в записи сходится с таблицей
|
||||
`adr/README.md`;
|
||||
- **замена парная**: новая запись пересматривает прежнее решение — у старой
|
||||
обязан быть статус «заменено на». Односторонняя замена оставляет две
|
||||
активные записи об одном, и `architecture` прочитает ту, что нашёл первой.
|
||||
|
||||
7. **Пустое названо пустым, а не заглушено.** Незаполненный документ канона
|
||||
держит **одну честную информативную строку**: «внешних зависимостей нет —
|
||||
смотри на диск и на СУБД». Плейсхолдеры шаблона ловит машина; твоё — строка,
|
||||
которая **есть, но ничего не сообщает**: «TBD», «будет дополнено», «раздел в
|
||||
работе», а также честная по форме, но пустая по содержанию («зависимости
|
||||
описаны ниже» при отсутствии «ниже»). Предлагай готовую строку — ту, которую
|
||||
проход ревью прочитает **как факт** и не потратит на неё обязательный вопрос.
|
||||
|
||||
8. **`security.md` начинается периметром.** «Сервис открыт наружу» и «контур
|
||||
доверенный» — противоположные постановки под одним заголовком, и враждебный
|
||||
проход между ними сам не выберет. Периметра нет в первых строках — находка.
|
||||
Контур ещё не развёрнут — обязаны быть названы **оба** периметра, целевой и
|
||||
сегодняшний, и сказано прямо, против какого строятся находки.
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
Не своё бывает трёх родов, и поступают с ними по-разному.
|
||||
|
||||
**Чужому подрядчику — строкой в границах покрытия.** Соответствие документов
|
||||
коду у `doc-code-drift`; язык (залог, оценки, англицизмы, жаргон, неизвестный
|
||||
термин, слово в двух смыслах) у `doc-wording`; форма записи задач у `task-form`.
|
||||
Увидел — назови в конце одной строкой, чтобы находка не пропала, но находкой не
|
||||
оформляй: две проверки одного места расходятся и начинают спорить.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловят `docs.py check` и
|
||||
`tasks.py check` (отсутствующие пути канона, файлы вне канона, имена файлов и
|
||||
форма имени ADR, битые ссылки, версия канона, нетронутые плейсхолдеры, число
|
||||
маркеров долга, миграция без правки `database.md`, capability без упоминания),
|
||||
**не пиши даже строкой**: это не потерянная находка, а уже проверенное.
|
||||
|
||||
**Верность решений.** Правильно ли выбрана архитектура, достаточна ли модель
|
||||
угроз, разумен ли инвариант — это ревью, а не сверка. Документ, внутренне
|
||||
согласованный и целиком неверный, для тебя чист, и это не твой промах.
|
||||
|
||||
## Порог вмешательства
|
||||
|
||||
**Находка без нарушенного правила не делается.** «Мне кажется, тут стоило бы
|
||||
подробнее» — не находка. Список, где половина пунктов вкусовые, перестают читать
|
||||
целиком, и вместе с ним пропадают настоящие расхождения.
|
||||
|
||||
**Второй дом — только там, где два текста утверждают.** Ссылка на другой документ
|
||||
вторым домом **не является**, и упоминание факта в проходящей фразе («см.
|
||||
периметр в `security.md`») тоже. Правило написано против расхождения, а не против
|
||||
слов.
|
||||
|
||||
**Сомневаешься, какой из двух домов канонический, — не выбирай.** Назови оба и
|
||||
скажи, что карта домов ответа не даёт: это находка о самом каноне, и она
|
||||
ценнее угаданной.
|
||||
|
||||
## Доклад
|
||||
|
||||
Находки по одной, в порядке важности: прямые противоречия → факт в двух домах →
|
||||
поведение в обзоре → ADR и провенанс → пустые слоты. Первые ломают решения,
|
||||
которые по документам принимают; последние — только цену чтения.
|
||||
|
||||
```
|
||||
<файл> ↔ <файл> (или <файл> — для одиночных)
|
||||
правило: <номер и короткое имя>
|
||||
сейчас: <что утверждает каждый>
|
||||
дом по канону: <адрес> — <почему он>
|
||||
предложение: <готовая формулировка либо строка-ссылка на замену копии>
|
||||
```
|
||||
|
||||
В конце — **границы покрытия**: сколько документов просмотрено из скольких, какие
|
||||
не смотрел и почему, читались ли спеки и архив изменений. Отчёт без этой строки
|
||||
читается как «канон сверен», не сообщая, какая его часть осталась нетронутой.
|
||||
Туда же — строка «замечено не по моей части»; машинно проверяемое в неё **не
|
||||
идёт**.
|
||||
|
||||
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
|
||||
полезнее выдуманного противоречия.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: doc-wording
|
||||
description: "Вычитка языка проектных текстов по информационному стилю — документы канона, решения ADR, записки разведки, задачи и цели, вперемешку тоже. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта. Отдаёт готовые формулировки на замену и ничего не правит сам. Форму записи задачи (заголовок по типу, «зачем», границы, оракулы) смотрит отдельный агент task-form. Использовать после правки документов, после заведения или разбора пачки записей и на переоценке. Только чтение."
|
||||
description: "Вычитка языка проектных текстов по информационному стилю — документы канона, решения ADR, записки разведки, задачи и цели, вперемешку тоже. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта, транслит в имени файла. Отдаёт готовые формулировки на замену и ничего не правит сам. Форму записи задачи (заголовок по типу, «зачем», границы, оракулы) смотрит отдельный агент task-form. Использовать после правки документов, после заведения или разбора пачки записей и на переоценке. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: sonnet
|
||||
color: green
|
||||
@@ -97,7 +97,35 @@ color: green
|
||||
|
||||
<!-- /копия: язык-англицизмы -->
|
||||
|
||||
6. **Жаргон и метафоры заменяются прямым называнием.**
|
||||
6. **Слово из своего словаря не трогается — список закрыт.**
|
||||
|
||||
<!-- копия: язык-словарь из av-dev-pm/skills/canon/references/language.md -->
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | ступень конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка, а
|
||||
не «принятый стиль»: у него либо есть живой русский аналог, либо оно требует
|
||||
ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не надо:
|
||||
**конфляция** (смешение), **декорреляция** (разведённость, разведён с кем-то),
|
||||
**непоймание** (почему не поймали), **эвал-сет** (проверочный набор), **гайд**
|
||||
(руководство). Каждое было латинизмом или калькой при живом русском слове, и
|
||||
каждое к моменту снятия жило в трёх-шести файлах разом — то есть выглядело
|
||||
словарём, не будучи им.
|
||||
|
||||
<!-- /копия: язык-словарь -->
|
||||
|
||||
7. **Жаргон и метафоры заменяются прямым называнием.**
|
||||
|
||||
<!-- копия: язык-жаргон из av-dev-pm/skills/canon/references/language.md -->
|
||||
|
||||
@@ -114,7 +142,7 @@ color: green
|
||||
|
||||
<!-- /копия: язык-жаргон -->
|
||||
|
||||
7. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
8. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
употребляется.** Заменять его своей догадкой нельзя: ты не знаешь предметную
|
||||
область. Пиши «термин «X» не встречается ни в документах, ни в других
|
||||
поданных файлах — введи строкой или назови известным словом».
|
||||
@@ -122,13 +150,26 @@ color: green
|
||||
**Слово, занятое в другом смысле, — та же находка.** Термин, который в одном
|
||||
документе проекта значит одно, а здесь другое, ломает оба; назови оба места.
|
||||
|
||||
9. **Имя файла — английское слово по сути, а не транслит.** `queue-as-table`, а
|
||||
не `ochered-tablicej`; `move-parse-strict`, а не `razbor-hoda`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается, а имя стоит в ссылках,
|
||||
коммитах и путях, которые набирают руками.
|
||||
|
||||
Кириллицу в имени и не-kebab-case ловят `docs.py` и `tasks.py` — про них
|
||||
молчи. Твоё — **транслит**, потому что машина проверяет его эвристикой и
|
||||
ловит не всё: `sostoyanie-partii` проходит мимо неё. Находка — готовое
|
||||
английское имя на замену плюс напоминание, что переименование это **перенос
|
||||
ссылок одним проходом**, а не правка одного файла.
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
||||
|
||||
**Чужому подрядчику — строкой в границах покрытия.** Форма записи задачи у
|
||||
`task-form`; увидел — назови в конце одной строкой, чтобы находка не пропала, но
|
||||
находкой не оформляй.
|
||||
`task-form`; согласованность документов между собой (факт в двух домах,
|
||||
противоречие, поведение в обзоре) у `doc-consistency`; соответствие документов
|
||||
коду у `doc-code-drift`. Увидел — назови в конце одной строкой, чтобы находка не
|
||||
пропала, но находкой не оформляй.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловят `tasks.py check` и
|
||||
`docs.py check` (состав и написание секций, наличие разделов, число критериев,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: task-form
|
||||
description: "Проверка формы записи каталога задач по существу: форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, идея — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент doc-wording. Использовать после заведения или разбора пачки записей, до взятия в спринт и на переоценке. Только чтение."
|
||||
description: "Проверка формы записи каталога задач по существу: тип, разошедшийся с содержанием записи, форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, разведка — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент doc-wording. Использовать после заведения или разбора пачки записей, до взятия в спринт и на переоценке. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: opus
|
||||
color: yellow
|
||||
@@ -29,7 +29,7 @@ color: yellow
|
||||
|
||||
Список файлов записей (`docs/tasks/items/<slug>.md`) или каталог задач целиком.
|
||||
Каталог тебе нужен и сам по себе: задача несёт тег `goal:<слаг>`, и **файл цели
|
||||
ты открываешь**, иначе шестое правило не проверить.
|
||||
ты открываешь**, иначе седьмое правило не проверить.
|
||||
|
||||
Документы проекта — паспорт, архитектура, конвенции — если зовущий их назвал.
|
||||
По ним видно, названа ли граница именем, которое в проекте существует.
|
||||
@@ -38,11 +38,14 @@ color: yellow
|
||||
|
||||
1. **Заголовок отвечает на вопрос своего типа.**
|
||||
|
||||
Тип стоит первым полем меты — `- **Тип:** …`, — а в заголовке ему
|
||||
соответствует эмодзи.
|
||||
|
||||
| Тип | Отвечает на | Форма |
|
||||
| --- | --- | --- |
|
||||
| `[goal]` | что приложение будет уметь | утверждение о возможности: «Соперником может быть компьютер» |
|
||||
| задача | что нужно сделать | глагол в неопределённой форме, допускается «не» перед ним: «Печатать поле одним куском кода» |
|
||||
| `[idea]` | о чём она | назывное, без обещания: «Подсказка следующего хода» |
|
||||
| 🎯 `goal` | что приложение будет уметь | утверждение о возможности: «Соперником может быть компьютер» |
|
||||
| ✨ `feature`, 🐞 `fix`, 🧹 `chore` | что нужно сделать | глагол в неопределённой форме, допускается «не» перед ним: «Печатать поле одним куском кода» |
|
||||
| 🔬 `research` | о чём разведка | назывное, без обещания: «Подсказка следующего хода» |
|
||||
|
||||
Описательный заголовок задачи («Лишние символы молча отбрасываются») называет
|
||||
**состояние** и одинаково читается как жалоба и как задание. Заголовок цели в
|
||||
@@ -55,12 +58,32 @@ color: yellow
|
||||
законная возможность**: «исход слияния не зависит от порядка доставки» — цель,
|
||||
а не абстракция.
|
||||
|
||||
2. **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль,
|
||||
2. **Тип сходится с тем, что в записи написано.** Тип — первое поле меты, и он
|
||||
решает, каких разделов запись требует; разошедшийся тип врёт ровно там, где
|
||||
по нему принимают решение. Проверяемые расхождения:
|
||||
|
||||
- **`fix`, у которого нечего воспроизвести**, — расхождение приняли на слово.
|
||||
Либо это `research` («при каких условиях проявляется»), либо `feature`:
|
||||
поведение никогда и не было заявлено, и чинить нечего;
|
||||
- **`feature`, после которой снаружи ничего не меняется**, — это `chore`, и
|
||||
сказать это честно дешевле, чем выдумывать пользовательскую пользу;
|
||||
- **`chore`, меняющий наблюдаемое поведение**, — это `feature` или `fix`, и у
|
||||
них другие требования (цель, воспроизведение);
|
||||
- **`research`, у которого «Вопрос» — это тема, а не вопрос.** «Разобраться с
|
||||
выводом в терминалах» вопросом не является: на него нельзя ответить. Пока
|
||||
вопроса нет, запись остаётся сырьём — и это законное состояние, но назови
|
||||
его.
|
||||
|
||||
Раздел не из схемы своего типа (`Воспроизведение` у `chore`, критерии у
|
||||
`research`) — сигнал того же расхождения, и `check` о нём говорит замечанием.
|
||||
Твоя работа — сказать, **какой тип верен**, а не только что текущий не сходится.
|
||||
|
||||
3. **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль,
|
||||
— а не пересказывает заголовок. «Починить разбор хода» при заголовке «Не
|
||||
отбрасывать молча лишние символы» — пересказ: читающий узнаёт то же самое
|
||||
дважды и по-прежнему не знает, почему это лежит в беклоге.
|
||||
|
||||
3. **«Затрагивает» перечисляет границы, а не замысел.** Граница — то, у чего есть
|
||||
4. **«Затрагивает» перечисляет границы, а не замысел.** Граница — то, у чего есть
|
||||
внешняя сторона: команда и её аргументы, эндпоинт, таблица и миграция, формат
|
||||
на диске, публичный тип пакета, внешний сервис. «Переписать хранилище на новый
|
||||
драйвер» — замысел; проверяется вопросом «это можно назвать до того, как
|
||||
@@ -72,17 +95,17 @@ color: yellow
|
||||
становится двумя» вместо «выбор источника хода в модуле партии») — это уже
|
||||
решение о том, как делать.
|
||||
|
||||
4. **У критерия назван оракул, и оракул проверяем.** «Оракул: глазами» на
|
||||
5. **У критерия назван оракул, и оракул проверяем.** «Оракул: глазами» на
|
||||
утверждение, которого глазами не проверить («компьютер не проигрывает ни в
|
||||
одной партии»), — находка: слово стоит, проверки нет. Число критериев считает
|
||||
`tasks.py check`, тебе оно неинтересно.
|
||||
|
||||
5. **Предписания процесса в теле нет.** «Делать профилем standard», «взять
|
||||
6. **Предписания процесса в теле нет.** «Делать профилем standard», «взять
|
||||
такой-то агент» — это выбор, который делают, увидев изменение, а не при
|
||||
постановке. Он же путь понизить требования решением, принятым до
|
||||
проектирования.
|
||||
|
||||
6. **Задача называет, какую строку «Завершения» своей цели она двигает.**
|
||||
7. **Задача называет, какую строку «Завершения» своей цели она двигает.**
|
||||
Открой файл цели из тега `goal:<слаг>` и сверь. Три исхода, и все три —
|
||||
разные находки:
|
||||
|
||||
@@ -94,16 +117,18 @@ color: yellow
|
||||
это незакрытая часть возможности. Скажи о ней отдельно, вне списка находок
|
||||
по файлам: это про набор, а не про запись.
|
||||
|
||||
У задачи **без цели** (`kind:fix`, `chore`, `research`) правило не
|
||||
применяется вовсе — они служат работоспособности, а не направлению.
|
||||
У задачи **без цели** (`fix`, `chore`, `research`) правило не применяется
|
||||
вовсе — они служат работоспособности, а не направлению.
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
||||
|
||||
**Чужому подрядчику — строкой в границах покрытия.** Язык у `doc-wording`;
|
||||
увидел — назови в конце одной строкой, чтобы находка не пропала, но находкой не
|
||||
оформляй.
|
||||
согласованность документов канона между собой у `doc-consistency`, их
|
||||
соответствие коду у `doc-code-drift` — до задач эти двое не доходят вовсе, но
|
||||
если ты открыл цель и увидел расхождение в самом документе, оно их. Увидел —
|
||||
назови в конце одной строкой, чтобы находка не пропала, но находкой не оформляй.
|
||||
|
||||
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (наличие
|
||||
разделов, число критериев, состав и написание секций, теги, тег `question` при
|
||||
@@ -113,7 +138,7 @@ color: yellow
|
||||
одного правила.
|
||||
|
||||
**Содержание работы**: нужна ли задача, верно ли выбрана цель, не крупна ли она,
|
||||
достаточна ли декомпозиция. Шестое правило подходит к этому близко и
|
||||
достаточна ли декомпозиция. Седьмое правило подходит к этому близко и
|
||||
останавливается там, где кончается сверка с текстом цели. Об этом молчи.
|
||||
|
||||
## Порог вмешательства
|
||||
|
||||
@@ -18,7 +18,7 @@ description: Привести проект к канону документов
|
||||
которое прочитали последним. Прочитай его **до** первой правки.
|
||||
|
||||
- [references/skeletons.md](references/skeletons.md) — **что именно класть** в
|
||||
каждый незаполненный слот. Своей формой заглушку не выдумывай: `docs.py`
|
||||
каждый незаполненный слот. Не выдумывай заглушку своей формы: `docs.py`
|
||||
узнаёт только плейсхолдер `<!-- заполнить: … -->` из шаблонов.
|
||||
- [references/language.md](references/language.md) — **как это написано словами**:
|
||||
информационный стиль, применённый к проектным текстам, таблицы англицизмов и
|
||||
@@ -67,23 +67,29 @@ python3 $ds version --dir <корень> # версия кано
|
||||
capability: незаполненный канон это переходное состояние, а не отказ. Маркеры
|
||||
долга просто считает числом.
|
||||
|
||||
**Ты** судишь о том, чего она не умеет:
|
||||
Того, чего она не умеет, **ты не судишь сам** — для этого есть два агента, и
|
||||
разведены они по глубине:
|
||||
|
||||
- **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что
|
||||
capability `recognition`. Файлы разные, содержание одно;
|
||||
- **поведение, оставшееся в `architecture.md`** — раздел на 900 строк с
|
||||
требованиями вместо обзора;
|
||||
- **достаточность честной строки** — «внешних зависимостей нет» это факт,
|
||||
«TBD» — пробел;
|
||||
- **протухший факт** — документ ссылается на то, чего в коде уже нет.
|
||||
| Агент | Что смотрит | Читает |
|
||||
| --- | --- | --- |
|
||||
| `doc-consistency` | смысловой дубль, прямое противоречие между документами, поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса, число без провенанса, заглушка вместо честной строки | `docs/`, `openspec/` |
|
||||
| `doc-code-drift` | протухший факт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки проекта, capability | весь репозиторий |
|
||||
|
||||
Судит **не тот, кто писал**: самопроверка документа слабее всего ровно там, где
|
||||
формулировка казалась удачной при написании. Ни один из них ничего не правит —
|
||||
оба возвращают готовые формулировки, подставляешь ты.
|
||||
|
||||
## `check`
|
||||
|
||||
1. `docs.py check`, при наличии базы диффа — с `--base`.
|
||||
2. Прочитай то, что скрипт проверить не может (список выше), по документам,
|
||||
которых касалась работа. Не «заодно по всему `docs/`».
|
||||
3. Доклад: вывод скрипта строкой исхода, твои находки поимённо, **граница
|
||||
покрытия** — что смотрел и чего не смотрел.
|
||||
2. **Агентов на каждом `check` не зови.** Оба — `doc-consistency` и
|
||||
`doc-code-drift` — зовутся раз в спринт (шаг сессии), а также шагом 6 `adopt`
|
||||
и шагом 6 `upgrade`, на весь канон разом. Они дороги: оба на `opus`, второй
|
||||
ещё и читает репозиторий. Позвал `doc-code-drift` — передай ему раздел
|
||||
запретов `CLAUDE.md`.
|
||||
3. Доклад: вывод скрипта строкой исхода, находки агентов поимённо, **граница
|
||||
покрытия** — что смотрели и чего не смотрели, и **кого из двоих позвал**:
|
||||
доклад, умолчавший об этом, читается как «сверено».
|
||||
|
||||
Дрейф раскладки чинится переносом; смысловые находки — это либо правка
|
||||
документа, либо задача, если работы больше чем на абзац.
|
||||
@@ -95,7 +101,7 @@ capability: незаполненный канон это переходное с
|
||||
`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый
|
||||
уезжает. **Но смотрит он только верхний уровень `docs/`:** упразднённое в корне
|
||||
репозитория (`BRIEF.md`) и во вложенных каталогах он не назовёт никогда, поэтому
|
||||
корневые `*.md` читаются глазами. Плюс: `CLAUDE.md`, `openspec/specs/` (список
|
||||
корневые `*.md` читай глазами. Плюс: `CLAUDE.md`, `openspec/specs/` (список
|
||||
capability), `openspec/config.yaml`.
|
||||
|
||||
### 2. Составь карту
|
||||
@@ -131,8 +137,8 @@ capability), `openspec/config.yaml`.
|
||||
незаполненное — одной честной информативной строкой, а не «TBD»;
|
||||
3. переносы содержимого;
|
||||
4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он
|
||||
владеет форматом задач, включая переименование транслитных слагов в
|
||||
английские вместе с починкой перекрёстных ссылок;
|
||||
владеет форматом задач. Он же переименует транслитные слаги в английские и
|
||||
тем же проходом починит перекрёстные ссылки;
|
||||
5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
|
||||
`CLAUDE.md`, `README.md`;
|
||||
6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
|
||||
@@ -161,6 +167,20 @@ capability), `openspec/config.yaml`.
|
||||
содержания, сколько маркеров долга в `architecture.md`, сколько задач без
|
||||
критериев приёмки. Закрывается порциями по ходу работы, а не одним заходом.
|
||||
|
||||
### 6. Позови обоих судей
|
||||
|
||||
`docs.py` увидел раскладку, а не смысл: перенос растащил один факт по двум домам,
|
||||
оставил в `architecture.md` поведение, которому место в спеке, и оторвал ADR от
|
||||
его `design.md`. Ничего из этого скрипт не видит, и первый прогон на живом
|
||||
проекте обычно самый урожайный — правило единственного дома до адаптации никто не
|
||||
проверял.
|
||||
|
||||
Зови **`doc-consistency`** (документы между собой и с openspec) и
|
||||
**`doc-code-drift`** (факты против кода). Разбирай порциями, а не одним заходом.
|
||||
|
||||
**Передай им объявленное переходное состояние из шага 5** — иначе честная строка
|
||||
в незаполненном слоте вернётся находкой, а это не поломка, а объявленный долг.
|
||||
|
||||
## `upgrade` — канон вырос
|
||||
|
||||
1. `docs.py version` — версия проекта и версия скрипта.
|
||||
@@ -171,10 +191,21 @@ capability), `openspec/config.yaml`.
|
||||
применяются по порядку.
|
||||
4. Подними `canon` в `docs/.pm.json` до текущей.
|
||||
5. `docs.py check`.
|
||||
6. **Позови обоих судей** — `doc-consistency` и `doc-code-drift`.
|
||||
|
||||
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
|
||||
это дефект журнала, и о нём надо сказать, а не догадываться.
|
||||
|
||||
**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.pm.json` с
|
||||
версией скрипта — и только его. Применена ли запись журнала **по существу**, он
|
||||
не знает: проект несёт `"canon": 4` и может не иметь того, чего требовала любая
|
||||
из пройденных версий. Записи применяются руками (переименовать секцию, проставить
|
||||
типы, дописать раздел каждому `fix`), а ручной проход по нескольким записям
|
||||
подряд — ровно то место, где половина шага делается и забывается. Судьи и есть
|
||||
проверка, которой у `upgrade` иначе нет: `doc-consistency` увидит, что документы
|
||||
разошлись после переименований, `doc-code-drift` — что переехавший факт
|
||||
разошёлся с кодом.
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
- **Не сочиняет содержание.** Пустой слот получает честную строку о том, что его
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Канон документов проекта
|
||||
|
||||
**Версия 3.**
|
||||
**Версия 4.**
|
||||
|
||||
Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs`
|
||||
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
|
||||
@@ -22,6 +22,29 @@
|
||||
[language.md](language.md): информационный стиль, англицизмы, жаргон. Он
|
||||
относится и к задачам, и к решениям ADR, и к запискам разведки.
|
||||
|
||||
## Сопровождение и эксплуатация — целое и часть
|
||||
|
||||
Одна тема живёт в трёх местах канона, и путать их слова нельзя.
|
||||
|
||||
**Сопровождение** — всё, чем держат проект: инструмент и сборка, процесс,
|
||||
выкладка, метрики и логи, инфраструктура, дежурство. **Эксплуатация** — его
|
||||
часть: работа системы на проде. Целое и часть, и никогда наоборот.
|
||||
|
||||
| Место | Уровень | Что там |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md`, секция `Сопровождение` | план | **работы**, которые собираемся делать: цели и их задачи |
|
||||
| `architecture.md`, раздел «Эксплуатация» | состояние | **как устроено сейчас**: где работает, что рядом, кто перезапускает |
|
||||
| эксплуатационный проход ревью | оптика | **чем проверяем**: «это упало через неделю на проде» |
|
||||
|
||||
Слово **«поддержка» не употребляется вовсе** — в нём слышится помощь
|
||||
пользователю, а это другая работа.
|
||||
|
||||
**Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
|
||||
сообщает о своём состоянии» — возможность приложения, её место среди прочих
|
||||
целей: наблюдает пользователь сервиса. «Дежурный видит состояние на одном
|
||||
экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в разные
|
||||
секции роадмапа, и это верно — секции отвечают на разные вопросы.
|
||||
|
||||
## Раскладка
|
||||
|
||||
```
|
||||
@@ -35,10 +58,10 @@ docs/
|
||||
security.md периметр; недоверенный вход; что вне модели
|
||||
conventions/
|
||||
README.md индекс, правило промоута, что механизировано
|
||||
<тема>.md
|
||||
<slug>.md
|
||||
research/
|
||||
README.md как снималось, индекс
|
||||
<тема>.md наблюдения и числа с провенансом
|
||||
<slug>.md наблюдения и числа с провенансом
|
||||
adr/
|
||||
README.md индекс записей, статусы, правило замены
|
||||
template.md
|
||||
@@ -52,8 +75,29 @@ openspec/
|
||||
changes/archive/ архив изменений с design.md — сырьё для ADR
|
||||
```
|
||||
|
||||
Текст документов — русский; слаги файлов, capability и задач — английские,
|
||||
kebab-case.
|
||||
### Имена файлов английские, текст русский
|
||||
|
||||
**Текст документов русский; имена файлов, capability и задач — английские,
|
||||
kebab-case.** Причина не эстетическая: имя файла стоит в ссылках из других
|
||||
документов, в коммитах и в путях, которые набирают руками, — а кириллица в пути
|
||||
ломается по-разному в разных местах и не набирается на английской раскладке.
|
||||
|
||||
**Транслита не заводим.** Слаг именуется английским словом **по сути**, а не
|
||||
записью русского латиницей: `queue-as-table`, а не `ochered-tablicej`. Транслит
|
||||
нечитаем тому, кто ищет по смыслу, и не сокращается.
|
||||
|
||||
У ADR имя вдобавок несёт форму — `ADR-ГГГГ-ММ-ДД-slug.md`: по ней записи
|
||||
сортируются, и по ней же ищется дата решения.
|
||||
|
||||
`docs.py check` проверяет кириллицу и kebab-case **жёстко**, форму имени ADR —
|
||||
тоже, а транслит **эвристикой**, то есть замечанием: английское слово от
|
||||
транслита машина не отличает. Слаги каталога задач ведёт `tasks.py` — там та же
|
||||
проверка и тот же разрез.
|
||||
|
||||
**Переименование — не правка, а перенос ссылок**: делается одним проходом по
|
||||
всем местам, где имя упомянуто, иначе останутся битые ссылки. Для задач это
|
||||
умеет `tasks.py adopt`; для документов канона правит человек, а `docs.py` потом
|
||||
показывает, что ссылки целы.
|
||||
|
||||
## Роли документов
|
||||
|
||||
@@ -136,7 +180,7 @@ kebab-case.
|
||||
Прозой остаётся **только то, что не выражается правилом**. `README.md` держит
|
||||
индекс, правило промоута и **перечень уже механизированного** со ссылкой на
|
||||
место механизации — конфиг линтера, собственный анализатор, тест-сканер
|
||||
исходников. Непойманное место механизации означает, что проход добросовестно
|
||||
исходников. Не названное место механизации означает, что проход добросовестно
|
||||
проверит уже проверенное.
|
||||
|
||||
### `research/`
|
||||
@@ -199,7 +243,7 @@ kebab-case.
|
||||
проверять сознательно» (пересматривается первым).
|
||||
|
||||
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
|
||||
**проскочил / пойман ревью**. Проскочившие — эвал-сет для калибровки конвейера,
|
||||
**проскочил / пойман ревью**. Проскочившие — проверочный набор для калибровки конвейера,
|
||||
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
|
||||
воспроизводимые, однажды оказавшиеся правдой.
|
||||
|
||||
@@ -217,18 +261,36 @@ kebab-case.
|
||||
становится: нормативное поведение живёт в `openspec/specs/`, роадмап отвечает,
|
||||
**когда и в каком порядке** оно появилось.
|
||||
|
||||
Плюс два требования к записи задачи, потому что от них зависит, можно ли её
|
||||
оценить:
|
||||
**У каждой записи есть тип, и тип решает, что с ней можно делать.** Дом типа —
|
||||
поле меты `Тип` первой строкой; эмодзи в заголовке от него производна. Словарь
|
||||
закрыт:
|
||||
|
||||
- **род работы** тегом `kind:<род>` из закрытого словаря `feature` | `fix` |
|
||||
`chore` | `research` — у задачи обязателен, у цели запрещён. Он же решает,
|
||||
нужна ли цель: у `feature` обязательна, у остальных нет;
|
||||
- **раздел «Затрагивает»** в теле задачи — границы, которых изменение касается
|
||||
(эндпоинт, таблица и миграция, формат на диске, публичный тип пакета).
|
||||
| Тип | Что это |
|
||||
| --- | --- |
|
||||
| 🎯 `goal` | возможность приложения |
|
||||
| ✨ `feature` | снаружи появляется то, чего не было |
|
||||
| 🐞 `fix` | поведение расходится с заявленным |
|
||||
| 🧹 `chore` | обслуживание, поведение не меняется |
|
||||
| 🔬 `research` | исход — знание, а не изменение |
|
||||
|
||||
Оба требуются **к взятию в спринт**, а не к заведению: беклог пополняется чаще,
|
||||
**Схемы записи здесь нет намеренно.** Какие разделы тип требует, нужна ли ему
|
||||
цель и берётся ли он в спринт — скилл `tasks`: сводка в его
|
||||
[SKILL.md](../../tasks/SKILL.md), раздел «Тип записи», подробно — по файлу на
|
||||
тип в `tasks/references/task-<тип>.md`. Канон фиксирует **словарь**, потому что
|
||||
от него зависит, читается ли проект как продукт; схема — механика ведения задач,
|
||||
и второй её экземпляр разошёлся бы с первым (он и разошёлся: канон успел
|
||||
объявить цель у `fix` запрещённой, хотя она там необязательна).
|
||||
|
||||
Схема требуется **к взятию в спринт**, а не к заведению: беклог пополняется чаще,
|
||||
чем разбирается, и требование на входе выгоняло бы в заметки то, что должно
|
||||
лежать задачей.
|
||||
лежать задачей. Запись, не собравшая разделы своего типа, — законное состояние
|
||||
беклога; невзятой её делает `sprint take`.
|
||||
|
||||
Отдельного типа для незаполненной записи нет: «ещё не описано» — состояние, а не
|
||||
род работы, и называется оно **`research` без раздела «Вопрос»**. Такая запись в
|
||||
спринт не берётся и лежит в конце своей категории.
|
||||
|
||||
Раскладку, форму записи и алгоритм работы над каждым типом держит скилл `tasks`.
|
||||
|
||||
### `CLAUDE.md`
|
||||
|
||||
@@ -262,6 +324,7 @@ kebab-case.
|
||||
|
||||
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
|
||||
|
||||
<!-- дом: карта-домов -->
|
||||
| Факт | Дом |
|
||||
| --- | --- |
|
||||
| поведение системы | `openspec/specs/<capability>/spec.md` |
|
||||
@@ -276,6 +339,7 @@ kebab-case.
|
||||
| единые точки проекта | `architecture.md` |
|
||||
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
|
||||
| что уже механизировано правилом | `conventions/README.md` |
|
||||
<!-- /дом: карта-домов -->
|
||||
|
||||
## Пустое называется пустым
|
||||
|
||||
@@ -301,7 +365,7 @@ kebab-case.
|
||||
| --- | --- |
|
||||
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
|
||||
| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.md` (обзор) |
|
||||
| `docs/drafts/` | идея → задача `[idea]`; отказ → ADR; порядок → `ROADMAP.md`; размышление → `opsx:explore` |
|
||||
| `docs/drafts/` | идея → запись `research`; отказ → ADR; порядок → `ROADMAP.md`; размышление → `opsx:explore` |
|
||||
| `docs/plan.md` | `docs/tasks/ROADMAP.md` |
|
||||
| `BRIEF.md` | `passport.md` |
|
||||
| `docs/backlog/` | `docs/tasks/` |
|
||||
@@ -312,22 +376,43 @@ kebab-case.
|
||||
Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон
|
||||
соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего.
|
||||
|
||||
| Проверяет `docs.py` | Судит агент |
|
||||
| --- | --- |
|
||||
| отсутствующие пути канона | смысловой дубль документа и capability |
|
||||
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` |
|
||||
| битые относительные ссылки | протухший факт, разошедшийся с кодом |
|
||||
| версия канона и её отставание | достаточность честной строки в пустом слоте |
|
||||
| нетронутый плейсхолдер шаблона | связность и читаемость |
|
||||
| маркеры долга — числом | |
|
||||
| миграция изменена, а `database.md` нет | |
|
||||
| capability без упоминания в `architecture.md` | |
|
||||
| Проверяет `docs.py` | Судит агент | Какой |
|
||||
| --- | --- | --- |
|
||||
| отсутствующие пути канона | смысловой дубль документа и capability | `doc-consistency` |
|
||||
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` | `doc-consistency` |
|
||||
| имя файла не kebab-case латиницей; форма имени ADR | транслит в имени — сверх эвристики | `doc-wording` |
|
||||
| битые относительные ссылки | прямое противоречие между документами | `doc-consistency` |
|
||||
| версия канона и её отставание | достаточность честной строки в пустом слоте | `doc-consistency` |
|
||||
| нетронутый плейсхолдер шаблона | ADR без ссылки на `design.md`, замена без парного статуса | `doc-consistency` |
|
||||
| маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` |
|
||||
| миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` |
|
||||
| capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` |
|
||||
| | связность и читаемость | `doc-wording` |
|
||||
|
||||
**Агентов двое, и разведены они по глубине, а не по охвату.** `doc-consistency`
|
||||
читает только `docs/` и `openspec/`, `doc-code-drift` — весь репозиторий и гоняет
|
||||
читающие команды. Слитый агент делал бы одну половину поверхностной; тот же
|
||||
разрез, что между `task-form` и `doc-wording`.
|
||||
|
||||
**Зовутся оба одинаково — раз в спринт на сессии, а также после `adopt` и после
|
||||
`upgrade`, на весь канон разом.** Не на синке документации: агент на `opus` по
|
||||
каждой сделанной задаче не окупается, а расхождение между двумя документами по
|
||||
определению требует двух, и на большинстве задач синк правит один. Пачка,
|
||||
отбираемая работой, вдобавок не видит того, чего работа не касалась, — а именно
|
||||
там расхождение и живёт: правка отменяет решение в одном документе, парный статус
|
||||
нужен в другом.
|
||||
|
||||
**Перечень фактов, которые `doc-code-drift` сверяет с кодом, закрыт** — имя
|
||||
основной ветки, команды, пути, зависимости поимённо, настройки с числовым
|
||||
значением, единые точки проекта, capability, проверяемые инварианты. «Сверить
|
||||
архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт
|
||||
правдоподобную труху вместо находок.
|
||||
|
||||
## `docs/.pm.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"canon": 2,
|
||||
"canon": 4,
|
||||
"migrations": "internal/store/migrations",
|
||||
"tasks": {
|
||||
"backlog": "INDEX.md"
|
||||
@@ -341,10 +426,14 @@ kebab-case.
|
||||
`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего
|
||||
`<tasks>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
|
||||
Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`,
|
||||
`plan`, `sprint`, `rejected`, `sprint_section`, `questions_heading`,
|
||||
`criteria_heading`, `oracle_word`), и ключ пишется, лишь когда имя отличается от
|
||||
умолчания. **Секций беклога здесь нет:** их дом — заголовки `##` самого индекса,
|
||||
и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py`
|
||||
`roadmap`, `sprint`, `rejected`, `sprint_section`, `oracle_word` и заголовки
|
||||
разделов тела: `criteria_heading`, `surface_heading`, `questions_heading`,
|
||||
`completion_heading`, `repro_heading`, `question_heading`, `answer_heading`,
|
||||
`scope_heading`), и ключ пишется, лишь когда имя отличается от умолчания.
|
||||
**Словаря типов здесь нет** — он закрыт каноном, а не настраивается проектом:
|
||||
настраиваемый словарь типов разъехался бы на синонимах ровно так же, как
|
||||
открытый. **Категорий беклога здесь тоже нет:** их дом — заголовки `##` самого
|
||||
индекса, и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py`
|
||||
отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с
|
||||
задачами целиком.
|
||||
|
||||
|
||||
@@ -13,6 +13,128 @@ upgrade` идёт по записям снизу вверх от версии п
|
||||
|
||||
---
|
||||
|
||||
## Версия 4 — 2026-08-05
|
||||
|
||||
Две правки, обе про то, как читается каталог задач. Первая — секция роадмапа
|
||||
переименована, и вместе с именем расширен её смысл; достигнутое переехало вниз.
|
||||
Вторая — **у каждой записи появился тип, и тип определяет, что с записью можно
|
||||
делать**. Раскладка не меняется, файлов канона не прибавляется.
|
||||
|
||||
**Что переехало:**
|
||||
|
||||
- секция роадмапа `Разработка` → **`Сопровождение`** (англ. `Tooling` →
|
||||
**`Operations`**). Прежнее имя называло слишком много: роадмап **весь** про
|
||||
разработку, и секция с таким именем не отличалась от остальных ничем;
|
||||
- **тип записи** — из префикса заголовка (`[goal]`/`[idea]`) и тега
|
||||
`kind:<род>` в **поле меты `Тип`** первой строкой. Эмодзи в заголовке от него
|
||||
производна;
|
||||
- **поле места** у задачи: `Секция` → **`Категория`**. У цели остаётся `Секция`:
|
||||
у задачи поле называет полку домена, в которую она вернётся из спринта, у цели
|
||||
— часть роадмапа, то есть состояние очереди. Одно имя на два смысла их и
|
||||
смешивало.
|
||||
|
||||
**Что добавилось:**
|
||||
|
||||
1. **Смысл секции расширен.** Было «инструмент и процесс», стало «чем держат
|
||||
проект: инструмент, процесс, эксплуатация». Метрики, логи, инфраструктура,
|
||||
выкладка и дежурство — сюда же. Расширение не косметическое: английское
|
||||
`Operations` при узком смысле обещало бы эксплуатацию, а внутри лежал бы
|
||||
линтер.
|
||||
2. **Общий словарь трёх мест** — [canon.md](canon.md), раздел «Сопровождение и
|
||||
эксплуатация». Сопровождение — всё, чем держат проект; эксплуатация — его
|
||||
часть, работа системы на проде. `ROADMAP.md`, секция `Сопровождение` — план
|
||||
работ; `architecture.md`, раздел «Эксплуатация» — как устроено сейчас;
|
||||
эксплуатационный проход ревью — оптика проверки. Слить их в одно слово
|
||||
нельзя: они отвечают на разные вопросы. Слово **«поддержка» не употребляется
|
||||
вовсе** — в нём слышится помощь пользователю.
|
||||
3. **Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
|
||||
сообщает о своём состоянии» — возможность приложения, её место среди прочих
|
||||
целей. «Дежурный видит состояние на одном экране» — сопровождение. Одни и те
|
||||
же метрики попадают в разные секции роадмапа, и это верно.
|
||||
4. **Порядок секций стал каноническим**, и `Готово` переехало **вниз**:
|
||||
`Запланировано` | `Направления` | `Сопровождение` | `Готово`. Достигнутое
|
||||
копится — через год этой секции больше, чем всех остальных вместе, — и стоя
|
||||
первой она отодвигает за экран то, ради чего роадмап открывают чаще всего.
|
||||
Порядок проверяет `tasks.py check`, переставляет `check --fix`.
|
||||
5. **Заголовок секции отбивается пустой строкой с обеих сторон.** Прежде
|
||||
проверялась только строка после заголовка; перестановка секций двигает целые
|
||||
блоки, и два заголовка оказываются вплотную. Правит `check --fix`.
|
||||
6. **Тип — единственная ось записи, закрытый словарь из пяти значений:**
|
||||
`goal` | `feature` | `fix` | `chore` | `research`. Осей было две — тип записи
|
||||
(`goal`/`idea`/`task`) и род работы (`kind:` тегом), — но из двенадцати
|
||||
клеток произведения законны были шесть, а алгоритм работы крепится к роду, а
|
||||
не к типу. Оси схлопнуты.
|
||||
7. **Тип задаёт схему тела:** какие разделы обязательны, какие допустимы, нужна
|
||||
ли цель, берётся ли запись в спринт. Проверяет `sprint take`, замечания даёт
|
||||
`check`. Два раздела новые: **`Воспроизведение`** у `fix` (не
|
||||
воспроизводится — это `research`, а не `fix`; правило было записано и не
|
||||
проверялось) и **`Вопрос` + `Куда ляжет ответ`** у `research` вместо
|
||||
критериев приёмки (приёмка разведки — записанный ответ, и критерии в форме
|
||||
«оракул: тест» ей натянуты).
|
||||
8. **Тип `idea` упразднён.** Он значил не род работы, а состояние
|
||||
незаполненности, а состояние типом быть не может. Теперь оно называется
|
||||
честно: `research` без раздела «Вопрос» — **сырьё**. В спринт не берётся, как
|
||||
и прежняя идея, лежит **в конце своей категории** (проверяет `check`,
|
||||
переставляет `--fix`) и отбирается `list --raw`. Порядка «по важности» в
|
||||
беклоге по-прежнему нет: этот порядок производен от типа, а не назначен
|
||||
человеком.
|
||||
9. **Алгоритм работы над каждым типом** — отдельным файлом,
|
||||
`skills/tasks/references/task-<тип>.md`: схема, что проверяет машина, что
|
||||
человек, и порядок шагов.
|
||||
10. **Имена файлов проверяются.** Правило «текст русский, имена английские»
|
||||
стояло в каноне и не было подкреплено ничем: `docs.py` имён не смотрел вовсе.
|
||||
Теперь смотрит — кириллица и не-kebab-case **жёстко**, форма имени
|
||||
`ADR-ГГГГ-ММ-ДД-slug.md` жёстко, транслит **эвристикой**, то есть
|
||||
замечанием. Заодно из раскладки канона убраны плейсхолдеры `<тема>.md`,
|
||||
приглашавшие называть файлы по-русски.
|
||||
11. **Два агента вместо обещания.** В каноне была таблица «Что проверяет машина,
|
||||
а что человек», и её правая колонка три версии описывала судью, которого не
|
||||
существовало. Судьи заведены и разведены по глубине: **`doc-consistency`**
|
||||
(документ ↔ документ ↔ openspec: факт в двух домах, прямое противоречие,
|
||||
поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса,
|
||||
число без провенанса, заглушка вместо честной строки); **`doc-code-drift`**
|
||||
(документ ↔ код по закрытому перечню фактов). Оба зовутся раз в спринт на
|
||||
сессии, а также после adopt и после upgrade, на весь канон разом.
|
||||
|
||||
**Что сделать проекту:**
|
||||
|
||||
1. Переименовать заголовок секции в `docs/tasks/ROADMAP.md`: `## Разработка` →
|
||||
`## Сопровождение` (или `## Tooling` → `## Operations`, если индекс
|
||||
английский). **`check --fix` этого не сделает**: регистр канонической секции
|
||||
он правит сам, а чужую секцию только называет ошибкой — смысл за человеком.
|
||||
2. Поправить поле `- **Секция:**` в файлах целей, которые в ней лежат. Порядок
|
||||
именно такой: сперва заголовок, потом `python3 tasks.py check --dir
|
||||
docs/tasks` покажет расхождение поимённо.
|
||||
3. Перечитать состав секции: цели про выкладку, метрики, логи и инфраструктуру,
|
||||
если они лежали в `Направлениях` за неимением места, переезжают сюда.
|
||||
4. Прогнать `python3 tasks.py check --dir docs/tasks --fix`. За один проход он
|
||||
переставит секции роадмапа в канонический порядок (`Готово` уедет вниз вместе
|
||||
со всем содержимым), поправит отбивку заголовков и **переведёт записи на
|
||||
типы**: перенесёт значение из тега `kind:` и префикса `[goal]`/`[idea]` в поле
|
||||
`Тип`, снимет тег, поставит эмодзи в заголовок, переименует `Секция` →
|
||||
`Категория` у задач и снесёт сырьё в конец категорий.
|
||||
5. Разобрать то, что `--fix` вернул пометкой `НЕОДНОЗНАЧНО`. Главный случай —
|
||||
**записи без типа**: заведённые до появления рода работы, они не несут ни
|
||||
тега, ни префикса, и машина их не угадывает (`feature` от `chore` не
|
||||
отличает). Проставить руками: `edit <слаг> --type …`.
|
||||
6. Дописать новые обязательные разделы у задач, которые собираются в спринт:
|
||||
`Воспроизведение` у каждого `fix`, `Вопрос` и `Куда ляжет ответ` у каждого
|
||||
`research`. Не «заодно по всему беклогу», а порциями переоценки: `check`
|
||||
ошибкой это не считает, отказывает только `sprint take`. Сколько задач готово
|
||||
к взятию, печатает блок здоровья `check`.
|
||||
7. Прогнать `python3 docs.py check`: он назовёт имена файлов не по правилу.
|
||||
Кириллицу и не-kebab-case править обязательно, транслит — по решению
|
||||
человека. **Переименование ADR это перенос ссылок**: слаг стоит в
|
||||
`adr/README.md`, в `architecture.md` и в чужих документах, и делается одним
|
||||
проходом, иначе останутся битые ссылки (их `docs.py` потом и покажет).
|
||||
8. `docs/.pm.json`: `"canon": 4`.
|
||||
9. Позвать **обоих судей** — `doc-consistency` и `doc-code-drift`, шагом 6
|
||||
`upgrade`. Пунктов выше девять, половина из них ручная, и именно здесь видно,
|
||||
какие сделаны только наполовину: переименования секций и полей разводят
|
||||
документы, а `check` сверяет число версии, а не существо. Первый прогон на
|
||||
живом проекте вдобавок самый урожайный — правило единственного дома до сих пор
|
||||
никто не проверял. Разбирать порциями, а не одним заходом.
|
||||
|
||||
## Версия 3 — 2026-08-04
|
||||
|
||||
Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность
|
||||
@@ -92,7 +214,9 @@ upgrade` идёт по записям снизу вверх от версии п
|
||||
идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю
|
||||
частоту полного набора уточнением.
|
||||
7. Переименовать секции роадмапа: `порядок` → `Запланировано`, `темы` →
|
||||
`Направления`; завести `Готово` **первой** и `Разработка` последней.
|
||||
`Направления`; завести `Готово` **первой** и `Разработка` последней
|
||||
(порядок секций поменялся в версии 4 — если едешь сразу на неё, заводи
|
||||
`Готово` последней и не переставляй дважды).
|
||||
Прозаические разделы вроде «Что уже пройдено», которые велись руками,
|
||||
разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно),
|
||||
обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе
|
||||
|
||||
@@ -124,6 +124,38 @@
|
||||
|
||||
<!-- /дом: язык-англицизмы -->
|
||||
|
||||
## Свой словарь — закрытый список
|
||||
|
||||
Слово, не переводимое потому, что оно **имя вещи этого процесса**, а не украшение.
|
||||
Оговорка «термин прижился» без списка проверяема на глаз и потому не проверяема:
|
||||
прижившимся выглядит любое слово, встреченное трижды.
|
||||
|
||||
<!-- дом: язык-словарь -->
|
||||
| Термин | Что называет |
|
||||
| --- | --- |
|
||||
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||
| триаж | ступень конвейера, сводящая находки в решение |
|
||||
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||
| дифф, `--base` | разница между состояниями в git |
|
||||
| промпт | текст, которым зовут модель |
|
||||
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||
|
||||
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка, а
|
||||
не «принятый стиль»: у него либо есть живой русский аналог, либо оно требует
|
||||
ввода одной строкой при первом употреблении.
|
||||
|
||||
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не надо:
|
||||
**конфляция** (смешение), **декорреляция** (разведённость, разведён с кем-то),
|
||||
**непоймание** (почему не поймали), **эвал-сет** (проверочный набор), **гайд**
|
||||
(руководство). Каждое было латинизмом или калькой при живом русском слове, и
|
||||
каждое к моменту снятия жило в трёх-шести файлах разом — то есть выглядело
|
||||
словарём, не будучи им.
|
||||
|
||||
<!-- /дом: язык-словарь -->
|
||||
|
||||
## Жаргон и метафоры
|
||||
|
||||
Система не описывается внутренними метафорами и образными ярлыками: автору они
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
`adr/README.md` и `review.md` уезжают в репозиторий проекта и обязаны там что-то
|
||||
говорить; определение при этом остаётся в [canon.md](canon.md). Отсюда
|
||||
обязанность: **правка такого правила в каноне тянет запись в
|
||||
[changelog.md](changelog.md)** с указанием, какой файл проекта поднимает
|
||||
[changelog.md](changelog.md)** — и запись называет, какой файл проекта поднимает
|
||||
`upgrade`. Без этого копия в проекте останется на старой версии молча.
|
||||
|
||||
**Каждая такая копия помечена и сверяется машиной.** Дом обрамляется
|
||||
@@ -166,7 +166,7 @@
|
||||
| Правило | Где механизировано |
|
||||
| --- | --- |
|
||||
|
||||
Непойманное место механизации означает, что проход по конвенциям будет
|
||||
Не названное здесь место механизации означает, что проход по конвенциям будет
|
||||
добросовестно проверять уже проверенное.
|
||||
```
|
||||
|
||||
@@ -216,8 +216,9 @@
|
||||
|
||||
## Соглашения
|
||||
|
||||
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, слаг английский, дата — когда решение
|
||||
реально принято.
|
||||
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение реально принято.
|
||||
Слаг **английский по сути, а не транслитом**: `queue-as-table`, не
|
||||
`ochered-tablicej`. Форму имени и слаг проверяет `docs.py check`.
|
||||
- Записи неизменяемы: передумали — новая запись, старой ставится статус.
|
||||
- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и
|
||||
`устарело`; ставятся полем меты записи — `- **Статус:** …` рядом с датой и
|
||||
@@ -307,7 +308,7 @@
|
||||
## Журнал дефектов
|
||||
|
||||
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
|
||||
временем теряется не факт, а причина непоймания.
|
||||
временем теряется не факт, а то, почему дефект не поймали.
|
||||
|
||||
Форма:
|
||||
|
||||
@@ -392,7 +393,7 @@ severity стоит здесь, а не выводится каждым прох
|
||||
|
||||
```json
|
||||
{
|
||||
"canon": 2
|
||||
"canon": 4
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import NoReturn
|
||||
|
||||
CANON_VERSION = 3
|
||||
CANON_VERSION = 4
|
||||
|
||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||
|
||||
@@ -70,11 +70,110 @@ RETIRED = {
|
||||
"local-research.md": "→ docs/research/",
|
||||
"research.md": "→ docs/research/",
|
||||
"specs": "поведение → openspec/specs/, обзор → docs/architecture.md",
|
||||
"drafts": "идея → задача [idea], отказ → ADR, порядок → ROADMAP.md",
|
||||
"drafts": "идея → запись research, отказ → ADR, порядок → ROADMAP.md",
|
||||
"backlog": "→ docs/tasks/",
|
||||
"review": "→ docs/review.md",
|
||||
}
|
||||
|
||||
# --- Слаги в именах файлов --------------------------------------------------
|
||||
|
||||
# Текст документов русский, а **имена файлов английские, kebab-case**. Причина
|
||||
# не в эстетике: имя файла стоит в ссылках из других документов, в коммитах и в
|
||||
# путях, которые люди набирают руками, — а кириллица в пути ломается по-разному
|
||||
# в разных местах и не набирается на английской раскладке.
|
||||
SLUG = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
|
||||
ADR_NAME = re.compile(r"ADR-(\d{4})-(\d{2})-(\d{2})-(.+)")
|
||||
CYRILLIC = re.compile(r"[а-яёА-ЯЁ]")
|
||||
|
||||
# Признаки транслита — и только они. Отличить английское слово от транслита
|
||||
# машина не умеет, поэтому находка идёт **замечанием**: кластеры, которых в
|
||||
# английском практически не бывает, плюс окончания русских падежей.
|
||||
#
|
||||
# Слабые маркеры выброшены намеренно, каждый по своему ложному срабатыванию:
|
||||
# `ost` ловит `post` и `cost`, `sch` — `schema`, `ya` — `yaml`, `nost` —
|
||||
# `nostalgia`, хвост `ii` — `radii`. Набор подобран так, чтобы ложных
|
||||
# срабатываний не было вовсе: правило, краснеющее на правде, приучает
|
||||
# пролистывать весь блок. Цена известна и принята — `sostoyanie-partii`
|
||||
# проходит мимо.
|
||||
#
|
||||
# Тот же приём, что `translit_ish` в tasks.py; скрипты независимы намеренно —
|
||||
# каждый уезжает в чужой проект в одиночку.
|
||||
TRANSLIT_CLUSTER = re.compile(r"zh|kh|shch|tsy|iya|ovanie|enie|stvo")
|
||||
TRANSLIT_TAIL = re.compile(r"(?:ej|oj|ij|yj|yy|aya)$")
|
||||
|
||||
|
||||
def translit_ish(slug: str) -> bool:
|
||||
if TRANSLIT_CLUSTER.search(slug):
|
||||
return True
|
||||
return any(TRANSLIT_TAIL.search(part) for part in slug.split("-"))
|
||||
|
||||
|
||||
def check_slugs(root: Path, rep: Report) -> None:
|
||||
"""Имена файлов канона: латиница kebab-case, у ADR — ещё и форма имени.
|
||||
|
||||
Каталог задач не трогаем: его слаги ведёт и проверяет tasks.py, и вторая
|
||||
проверка того же места разошлась бы с первой.
|
||||
"""
|
||||
docs = root / "docs"
|
||||
if not docs.is_dir():
|
||||
return
|
||||
# Имена, выбранные каноном, а не проектом: их форма задана здесь же.
|
||||
fixed = {"README.md", "template.md"} | ALLOWED_FILES
|
||||
for sub in ("conventions", "research", "adr"):
|
||||
folder = docs / sub
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
for path in sorted(folder.rglob("*.md")):
|
||||
name = path.name
|
||||
rel = path.relative_to(root)
|
||||
if name in fixed:
|
||||
continue
|
||||
stem = path.stem
|
||||
if sub == "adr":
|
||||
m = ADR_NAME.fullmatch(stem)
|
||||
if not m:
|
||||
rep.error(
|
||||
f"{rel}: имя не по форме ADR-ГГГГ-ММ-ДД-slug.md — "
|
||||
f"по имени сортируются записи и ищется дата решения"
|
||||
)
|
||||
continue
|
||||
stem = m.group(4)
|
||||
if CYRILLIC.search(stem):
|
||||
rep.error(
|
||||
f"{rel}: кириллица в имени файла — слаги английские, "
|
||||
f"kebab-case (текст документа при этом русский)"
|
||||
)
|
||||
continue
|
||||
if not SLUG.fullmatch(stem):
|
||||
rep.error(
|
||||
f"{rel}: имя не kebab-case латиницей — только строчные "
|
||||
f"буквы, цифры и одиночные дефисы"
|
||||
)
|
||||
continue
|
||||
if translit_ish(stem):
|
||||
rep.note(
|
||||
f"{rel}: имя похоже на транслит («{stem}») — слаг именуется "
|
||||
f"английским словом по сути, а не записью русского латиницей: "
|
||||
f"транслит нечитаем тому, кто ищет по смыслу. Проверено "
|
||||
f"эвристикой: английское слово от транслита машина не отличает"
|
||||
)
|
||||
check_capability_slugs(root, rep)
|
||||
|
||||
|
||||
def check_capability_slugs(root: Path, rep: Report) -> None:
|
||||
specs = root / "openspec" / "specs"
|
||||
if not specs.is_dir():
|
||||
return
|
||||
for folder in sorted(specs.iterdir()):
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
if CYRILLIC.search(folder.name) or not SLUG.fullmatch(folder.name):
|
||||
rep.error(
|
||||
f"openspec/specs/{folder.name}/: имя capability — латиница "
|
||||
f"kebab-case; оно стоит в ссылках из architecture.md и в спеках"
|
||||
)
|
||||
|
||||
|
||||
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
|
||||
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
|
||||
MD_LINK = re.compile(r"\[[^\]]*\]\(\s*<?([^)>\s]+)>?(?:\s+[\"'(][^)]*)?\)")
|
||||
@@ -371,9 +470,10 @@ def report(rep: Report) -> int:
|
||||
print(f" {msg}")
|
||||
|
||||
print(
|
||||
"\nМашина проверила раскладку, ссылки, версию и две сверки с кодом.\n"
|
||||
"Смысловые дубли, оставшееся в architecture.md поведение и достаточность\n"
|
||||
"честной строки в пустом слоте она не проверяет — это суждение агента."
|
||||
"\nМашина проверила раскладку, имена файлов, ссылки, версию и две сверки\n"
|
||||
"с кодом. Согласованность документов между собой и с кодом она не\n"
|
||||
"проверяет — это суждение агентов `doc-consistency` (документ ↔ документ\n"
|
||||
"↔ openspec) и `doc-code-drift` (документ ↔ код)."
|
||||
)
|
||||
if rep.errors:
|
||||
print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.")
|
||||
@@ -394,6 +494,7 @@ def cmd_check(args: argparse.Namespace) -> int:
|
||||
check_version(root, cfg, rep)
|
||||
check_required(root, cfg, rep)
|
||||
check_stray(root, rep)
|
||||
check_slugs(root, rep)
|
||||
check_links(root, rep)
|
||||
check_placeholders_and_debt(root, rep)
|
||||
check_capabilities(root, rep)
|
||||
|
||||
@@ -21,8 +21,8 @@ description: Вести содержимое документов канона
|
||||
|
||||
Причина, по которой правило именно такое, измерена: у ADR был список триггеров
|
||||
прозой — и он дал **6 записей на 43 изменения**. Прозаический триггер, который
|
||||
некому проверить, не срабатывает. Умолчание «не написал» становится отличимым от
|
||||
«написал, что не требуется», только когда отрицание обязательно.
|
||||
некому проверить, не срабатывает. Отличить «не написал» от «написал, что не
|
||||
требуется» можно только тогда, когда отрицание обязательно.
|
||||
|
||||
Это тот же приём, что «границы покрытия» в отчёте ревью и «пустое называется
|
||||
пустым» в каноне.
|
||||
@@ -50,11 +50,29 @@ description: Вести содержимое документов канона
|
||||
Синк документации:
|
||||
- architecture.md — добавлен воркер свёртки, ссылка на capability reindex
|
||||
- database.md — миграция 00006, таблица bucket
|
||||
- adr/ — заведён ADR-2026-08-03-ochered-tablicej: отказ от внешней очереди
|
||||
- adr/ — заведён ADR-2026-08-03-queue-as-table: отказ от внешней очереди
|
||||
- research/ — новое о формате не узнано
|
||||
- passport, security, conventions, review — не требуется: изменение внутреннее
|
||||
```
|
||||
|
||||
## Сверка — не здесь, а на сессии
|
||||
|
||||
Синк правит документы поодиночке, а расходятся они **между собой**: факт,
|
||||
дописанный в `architecture.md`, уже живёт в `CLAUDE.md`; периметр в
|
||||
`security.md` не знает про новый эндпоинт. Поймать это на своей же правке нельзя,
|
||||
и судит это агент `doc-consistency`.
|
||||
|
||||
**Но синк его не зовёт.** Оба судьи документов — `doc-consistency` и
|
||||
`doc-code-drift` — зовутся раз в спринт, шагом сессии, на весь канон разом.
|
||||
Причина в цене: агент на `opus` по каждой сделанной задаче — самая дорогая
|
||||
церемония процесса. К тому же расхождение между двумя документами по определению
|
||||
требует двух документов, а на большинстве задач синк правит один.
|
||||
|
||||
Что теряется: привязка находки к задаче, которая её породила. Что выигрывается,
|
||||
кроме денег: синк перестаёт отбирать пачку, и в неё попадают документы,
|
||||
которых работа не касалась, — расхождение, внесённое правкой в одном месте, там
|
||||
и живёт.
|
||||
|
||||
## ADR — промоут, а не второе сочинение
|
||||
|
||||
Обоснование уже написано: `opsx:propose` кладёт `design.md` в каждый change, и
|
||||
@@ -83,7 +101,7 @@ description: Вести содержимое документов канона
|
||||
маркера долга и правило «гейт от них не краснеет» — в
|
||||
[каноне](../canon/references/canon.md), раздел `architecture.md`.**
|
||||
|
||||
Разбирается порциями: раздел вычищается той задачей, которая его касается.
|
||||
Разбирается порциями: раздел вычищает та задача, которая его касается.
|
||||
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
|
||||
обоснование в ADR, обзор остаётся строкой со ссылкой на capability.
|
||||
|
||||
@@ -109,8 +127,8 @@ description: Вести содержимое документов канона
|
||||
формы взять негде.
|
||||
|
||||
Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
|
||||
Со временем теряется не факт, а причина непоймания — единственное, ради чего
|
||||
журнал есть. И решение о сужении проверок (перестали звать проход, понизили
|
||||
Со временем теряется не факт, а то, почему дефект не поймали, — единственное,
|
||||
ради чего журнал есть. И решение сузить проверки (перестали звать проход, понизили
|
||||
профиль) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
|
||||
|
||||
## Промоут в конвенции
|
||||
|
||||
@@ -8,16 +8,16 @@ description: "Завести новый проект — сессия вопро
|
||||
Вход — свободный текст «что мне нужно и почему». Выход — канон документов, с
|
||||
которого дальше работают все остальные скиллы.
|
||||
|
||||
**Определение канона — [канон](../canon/references/canon.md).** Читается до
|
||||
**Определение канона — [канон](../canon/references/canon.md).** Прочитай его до
|
||||
первого вопроса: интервью идёт по слотам канона, а не по вкусу. Что класть в
|
||||
каждый файл — [скелеты](../canon/references/skeletons.md); своей формы заглушки
|
||||
не выдумывай, `docs.py` узнаёт только плейсхолдер оттуда.
|
||||
каждый файл — [скелеты](../canon/references/skeletons.md); не выдумывай заглушки
|
||||
своей формы, `docs.py` узнаёт только плейсхолдер оттуда.
|
||||
|
||||
## Что `init` физически не может произвести
|
||||
|
||||
В новом репозитории **нет кода**, а `architecture.md`, `database.md`,
|
||||
`conventions/` и `research/` выводятся из него. Их сочинение на старте — это
|
||||
проектирование вперёд реальности, и оно протухнет раньше первой задачи.
|
||||
`conventions/` и `research/` выводятся из него. Сочинить их на старте — значит
|
||||
проектировать вперёд реальности, и написанное протухнет раньше первой задачи.
|
||||
|
||||
Поэтому `init` заполняет то, что человек знает **до первой строки кода**:
|
||||
|
||||
@@ -56,13 +56,13 @@ description: "Завести новый проект — сессия вопро
|
||||
|
||||
- **Не больше трёх вопросов за итерацию** (`AskUserQuestion`), рекомендация
|
||||
первым вариантом. Между итерациями применяй уже решённое.
|
||||
- **Сперва вычитай ответы из брифа.** Вопрос, ответ на который в тексте уже
|
||||
есть, задавать не надо — покажи своё прочтение и спроси, верно ли.
|
||||
- **Сперва вычитай ответы из брифа.** Если ответ уже есть в тексте, вопрос не
|
||||
задавай — покажи своё прочтение и спроси, верно ли.
|
||||
- **Не выдумывай четыре вещи:** периметр, что необратимо, измеренные числа и
|
||||
адресата дорогой проверки. Их из замысла не вывести. Не сказано — пиши
|
||||
«неизвестно» с пометкой, что ждёт ответа.
|
||||
- **Развилка замысла — человеку, механика — сама.** Имена файлов, слаги, порядок
|
||||
строк не выносятся.
|
||||
строк не выноси.
|
||||
|
||||
## Порядок работы
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: session
|
||||
description: "Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги. Формат и содержимое задач — скилл tasks."
|
||||
description: "Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги, а также когда вернулись к проекту после перерыва и надо понять, где остановились. Формат и содержимое задач — скилл tasks."
|
||||
---
|
||||
|
||||
# Сессия между спринтами
|
||||
@@ -37,8 +37,10 @@ description: "Ритуал между спринтами и ведение са
|
||||
|
||||
## Единицы
|
||||
|
||||
- **Цель** — то, ради чего набирается спринт. Файл `[goal]`, перечисленный в
|
||||
`ROADMAP.md`. Цель постоянна: живёт, пока живёт направление.
|
||||
- **Цель** — то, ради чего набирается спринт. Файл типа `goal` (🎯),
|
||||
перечисленный в `ROADMAP.md`. Цель постоянна: живёт, пока живёт направление, —
|
||||
и уходит вместе с ним, если замысел оказался неверен (порядок отмены — в
|
||||
[tasks](../tasks/references/task-goal.md#отменённая-цель--сперва-задачи-потом-цель)).
|
||||
- **Задача** — то, что мерджится целиком и даёт видимую пользу.
|
||||
- **Вопрос** — решение человека. Не останавливает начатую работу, но **блокирует
|
||||
взятие** задачи в спринт. Живёт внутри файла задачи разделом «Вопросы» и тегом
|
||||
@@ -116,7 +118,9 @@ description: "Ритуал между спринтами и ведение са
|
||||
Это зависимость, а не список.
|
||||
|
||||
1. **Разбор вопросов.**
|
||||
2. **Разбор прошедшего спринта — про процесс, а не про задачи.**
|
||||
2. **Разбор прошедшего спринта — про процесс, а не про задачи.** Здесь же оба
|
||||
судьи документов канона на весь канон разом, раз в спринт: `doc-consistency`
|
||||
(документы между собой) и `doc-code-drift` (документы против кода).
|
||||
3. **Переоценка задач** порциями.
|
||||
4. **Выбор цели и набор спринта.** Цель называет человек, набор собирает агент и
|
||||
показывает **до старта работ**.
|
||||
@@ -143,6 +147,26 @@ flowchart TD
|
||||
Схема — **сводка**: процедура каждого шага в
|
||||
[references/cadence.md](references/cadence.md), и при расхождении прав текст.
|
||||
|
||||
## Вернулся, а спринт открыт
|
||||
|
||||
Сессия — ритуал **между** спринтами, и шаг 1 предполагает только что закрытый.
|
||||
Вход после перерыва другой, и начинается он не с шага, а с вопроса, свой ли ещё
|
||||
набор:
|
||||
|
||||
1. `tasks.py check` — блок здоровья скажет состояние спринта, число готовых к
|
||||
взятию и залежавшихся; при расхождении раскладки `--fix`.
|
||||
2. Прочитать `SPRINT.md`: цель, состав, дата начала.
|
||||
3. **Развилка, и решает её человек.** Набор всё ещё твой — продолжай спринт, ни
|
||||
сессии, ни переоценки не нужно, они между спринтами. Взялся перечитывать,
|
||||
зачем эти задачи собраны вместе, — набор протух:
|
||||
`sprint close --dissolve --reason …`, недоделанное возвращается в беклог,
|
||||
дальше обычная сессия с шага 1.
|
||||
|
||||
Порога в неделях нет намеренно — почему, в
|
||||
[references/sprint.md](references/sprint.md), «Протухший набор».
|
||||
Середины у развилки тоже нет: «доделаю пару штук и решу» — это работа по набору,
|
||||
которого ты уже не понимаешь.
|
||||
|
||||
Процедура каждого шага, размер и отбор порции, храповик на залежавшихся, формат
|
||||
интерактива и доклад — [references/cadence.md](references/cadence.md).
|
||||
|
||||
@@ -233,8 +257,8 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
||||
|
||||
## Слоты проекта
|
||||
|
||||
Сессия не знает ни языка, ни сборки, ни CI. Часть проектного отвечает
|
||||
[канон](../canon/references/canon.md) структурой: разбор процесса (шаг 2) живёт
|
||||
Сессия не знает ни языка, ни сборки, ни CI. На часть проектного отвечает своей
|
||||
структурой [канон](../canon/references/canon.md): разбор процесса (шаг 2) живёт
|
||||
в `docs/review.md`, оракулы и «чем краснеет безусловно» — в семантике гейта в
|
||||
`CLAUDE.md`. Остальное проект **дописывает в `CLAUDE.md`**:
|
||||
|
||||
@@ -249,8 +273,8 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
||||
это **ориентир, а не закон**.
|
||||
|
||||
Слота «куда копируются критерии приёмки» здесь нет намеренно: на него отвечает
|
||||
**пайплайн проекта**, перенося их в описание изменения при его заведении. Проект
|
||||
без пайплайна называет своё место сам, в слоте 1.
|
||||
**пайплайн проекта** — он переносит критерии в описание изменения, когда его
|
||||
заводит. Проект без пайплайна называет своё место сам, в слоте 1.
|
||||
|
||||
Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост
|
||||
беклога) — предмет шага 2, а не константы этого скилла.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
## Шаг 1. Разбор вопросов
|
||||
|
||||
`tasks.py list --questions` — всё, что накопилось. Вопрос это решение человека,
|
||||
и разбирается он **пачкой**, а не по одному в момент возникновения: по одному —
|
||||
и разбирается он **пачкой**, а не по одному, как только возник: по одному —
|
||||
это дёрганье, пачкой — это сессия.
|
||||
|
||||
Порядок по каждому вопросу:
|
||||
@@ -21,8 +21,8 @@
|
||||
2. **Сформулируй развилку** с вариантами и последствием каждого, рекомендация —
|
||||
первым вариантом.
|
||||
3. **Вынеси пачкой** через `AskUserQuestion`, не больше трёх за раз.
|
||||
4. **Ответ записывается в тело задачи, раздел «Вопросы» опустошается**, тег
|
||||
снимается `edit <slug> --rm-tag question`, **«зачем» переписывается**: «Решено:
|
||||
4. **Запиши ответ в тело задачи, опустоши раздел «Вопросы»**, сними тег
|
||||
(`edit <slug> --rm-tag question`), **перепиши «зачем»**: «Решено:
|
||||
…» на вопрос «почему это лежит в беклоге» уже не отвечает. Опустошение
|
||||
раздела — не уборка, а условие взятия: правило и причина в скилле `tasks`,
|
||||
[references/task-format.md](../../tasks/references/task-format.md).
|
||||
@@ -37,13 +37,15 @@
|
||||
Не «что мы сделали» (это доклад спринта, он уже был), а:
|
||||
|
||||
- **что сломалось в процессе и почему не поймали** — промах, доехавший до конца;
|
||||
- **сколько на самом деле заняли задачи** против ожидания;
|
||||
- **что оказалось дороже, чем выглядело при заведении** — не число, а сам факт и
|
||||
причина: чего не было видно в постановке;
|
||||
- **какие правила не сработали или сработали не так** — в том числе правила
|
||||
этого плагина;
|
||||
- **какие числа пора пересмотреть** — ориентир по размеру спринта, прирост
|
||||
беклога на одну закрытую задачу, время на задачу. Эта обязанность иначе висит
|
||||
ничья: числа, помеченные как «первый замер», не пересматриваются никогда, если
|
||||
их не пересматривает конкретный шаг.
|
||||
этого плагина.
|
||||
|
||||
Замеров процесс не ведёт намеренно: оценки в очках и velocity не взяты
|
||||
(«[Почему не Scrum](../SKILL.md#почему-не-scrum)»), а спринт ограничен объёмом, а
|
||||
не временем — сравнивать «сколько заняло» не с чем. Разбор здесь качественный, и
|
||||
это не упущение.
|
||||
|
||||
**Артефакт обязателен.** Вывод, оставшийся в контексте сессии, не существует:
|
||||
следующая сессия его не увидит. Дом у него один и известен из канона —
|
||||
@@ -54,6 +56,33 @@
|
||||
Отдельным ритуалом ретроспектива не выделяется: процесс личный,
|
||||
синхронизировать некого.
|
||||
|
||||
**Здесь же зовутся оба судьи документов** — на весь канон разом, а не на пачку,
|
||||
отобранную работой:
|
||||
|
||||
- **`doc-consistency`** — согласованность документов между собой и с openspec:
|
||||
факт в двух домах, прямое противоречие, поведение в `architecture.md` вместо
|
||||
спек, ADR без парного статуса при замене, число без провенанса;
|
||||
- **`doc-code-drift`** — сверка с кодом по закрытому перечню фактов: имя основной
|
||||
ветки, команды, пути, внешние зависимости поимённо, настройки с числовым
|
||||
значением, единые точки проекта, capability.
|
||||
|
||||
Раз в спринт, а не чаще, и причина в цене: оба на `opus`, а второй ещё и читает
|
||||
репозиторий. Но и не реже — **спринт это ровно то, что двигает код и документы**:
|
||||
переименованная цель сборки, ушедшая зависимость, второй способ делать то, что
|
||||
обзор объявил единственным; факт, дописанный в `architecture.md`, уже живущий в
|
||||
`CLAUDE.md`. Протухшее и раздвоившееся неотличимо от свежего, и по нему принимают
|
||||
решения, пока кто-нибудь не наткнётся.
|
||||
|
||||
**Пачка — весь канон, и это не расточительство, а охват.** Когда пачку отбирала
|
||||
работа, без присмотра оставалось ровно то, чего работа не касалась: правка,
|
||||
отменившая решение, живёт в одном документе, а парный статус нужен в другом.
|
||||
Канон мал, раз в спринт он читается целиком.
|
||||
|
||||
Находки обоих — обычный материал переоценки: строка на замену идёт в документ
|
||||
сразу, работа больше чем на абзац становится задачей типа `chore`. **Позвал —
|
||||
скажи в докладе, кого именно позвал, и приложи границы покрытия**; не позвал —
|
||||
скажи и это, иначе доклад читается как «сверено».
|
||||
|
||||
## Шаг 3. Переоценка задач
|
||||
|
||||
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
|
||||
@@ -105,29 +134,41 @@
|
||||
4. **Пере-кластеризуй по общей причине.** Несколько задач, оказавшихся симптомами
|
||||
одного дефекта, сливаются в одну — это находка, которую интейк дать не мог.
|
||||
5. **Гигиена полей** — протухшее «зачем», вопрос в прозе, снятый ответ, свойство
|
||||
репозитория в рамках, предписание процесса в теле, род работы, разошедшийся с
|
||||
репозитория в рамках, предписание процесса в теле, тип, разошедшийся с
|
||||
задачей, границы вместо реализации в разделе «Затрагивает». Список и правила —
|
||||
в скилле `tasks`. **Переоценка — то самое место, где беклог добирает род
|
||||
работы и границы:** требовать их на входе значило бы выгонять в заметки то,
|
||||
что должно лежать задачей, а к взятию в спринт они уже обязательны.
|
||||
в скилле `tasks`. **Переоценка — то самое место, где беклог добирает тип и
|
||||
разделы его схемы:** требовать их на входе значило бы выгонять в заметки то,
|
||||
что должно лежать задачей, а к взятию в спринт они уже обязательны. Блок
|
||||
здоровья `check` печатает, сколько записей готово к взятию, — по этому числу
|
||||
и видно, добрала переоценка или нет.
|
||||
|
||||
Затем — то, что решает пользователь:
|
||||
|
||||
6. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
|
||||
сценарий, обошли иначе. Здесь и звучит вопрос о выкидывании.
|
||||
сценарий, обошли иначе. Здесь и звучит вопрос, выкидывать ли.
|
||||
7. **Та ли цель — и нужна ли она вообще.** Приоритетов нет, и «повысить» нечего
|
||||
— вместо повышения **смена цели** (`edit <slug> --goal <другой>`) или
|
||||
включение в ближайший набор. `feature`, которой не находится цель, — кандидат
|
||||
— вместо повышения задача **меняет цель** (`edit <slug> --goal <другой>`) или
|
||||
входит в ближайший набор. `feature`, которой не находится цель, — кандидат
|
||||
на выход: новая возможность вне цели это возможность, которой никто не
|
||||
заказывал. Операционной задаче (`fix`, `chore`, `research`) цель не нужна, и
|
||||
выдумывать её здесь не надо.
|
||||
|
||||
**Отменяется и сама цель** — когда замысел оказался неверен, а не когда
|
||||
задача выбрала не ту. Тогда порция расширяется до всех задач этой цели: каждую
|
||||
либо закрыть своей причиной, либо перевесить на другую цель, и только потом
|
||||
закрыть цель. Порядок и почему он такой —
|
||||
[task-goal.md](../../tasks/references/task-goal.md#отменённая-цель--сперва-задачи-потом-цель).
|
||||
Здесь этому и место: отмена цели это разбор её задач, а разбор задач — этот
|
||||
шаг.
|
||||
8. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit
|
||||
<slug> --type idea`, дальше штурм. Разрослась → это несколько задач под той
|
||||
же целью, дальше декомпозиция.
|
||||
9. **Переоценка по измеренному.** Спринт производит числа — сколько на самом
|
||||
деле стоит такая работа, что оказалось дороже ожидания. Эти числа меняют цену
|
||||
**других** задач, и именно здесь это применяется: задача, чья цена выросла
|
||||
втрое, а польза осталась прежней, — кандидат на выход.
|
||||
<slug> --type research` и опустошённый раздел «Вопрос», то есть сырьё; дальше
|
||||
штурм. Разрослась → это несколько задач под той же целью, дальше
|
||||
декомпозиция.
|
||||
9. **Переоценка по пройденному.** Прошедший спринт показывает, чего на самом
|
||||
деле стоит такая работа. Это меняет цену **других** задач, и именно здесь
|
||||
применяется: задача, оказавшаяся заметно дороже, чем думалось, при прежней
|
||||
пользе — кандидат на выход. Судит человек по тому, что помнит о прошедшем
|
||||
спринте; замеров процесс не ведёт и оценок не хранит.
|
||||
|
||||
### Храповик на залежавшихся
|
||||
|
||||
@@ -167,7 +208,7 @@
|
||||
> - Взять в ближайший набор — без бэкапа ретеншн опасен
|
||||
> - Выкинуть
|
||||
> 3. `guessit-sputnik` — вынести распознавание в сервис-спутник
|
||||
> - Понизить до `[idea]` *(рекомендую)* — не проходит тест «готова к взятию»
|
||||
> - Понизить до сырья (`--type research`) *(рекомендую)* — не проходит тест «готова к взятию»
|
||||
> - Оставить задачей
|
||||
|
||||
Каждый вариант несёт причину — ту самую, что уедет в `--reason`. Ответы применяй
|
||||
@@ -184,15 +225,16 @@
|
||||
2. **Цель называет человек.** Это продуктовое решение, а не механика: агент
|
||||
предлагает и объясняет, но не выбирает.
|
||||
3. **Набор собирает агент** — `sprint start --goal <слаг>`, затем `sprint take
|
||||
…`. Скрипт не даст взять цель, идею, задачу с чужой целью, с открытым
|
||||
вопросом, без критериев приёмки, без рода работы или без раздела
|
||||
«Затрагивает». Задача без цели вовсе (`fix`, `chore`, `research`) берётся
|
||||
свободно — операционная работа входит в набор помимо его цели.
|
||||
…`. Скрипт не даст взять цель, задачу с чужой целью, с открытым вопросом, без
|
||||
типа и **без разделов, которых требует её тип** (у `fix` это в том числе
|
||||
`Воспроизведение`, у `research` — `Вопрос` и `Куда ляжет ответ`, и сырьё
|
||||
поэтому не берётся вовсе). Задача без цели (`fix`, `chore`, `research`)
|
||||
берётся свободно — операционная работа входит в набор помимо его цели.
|
||||
4. **Набор показывается человеку до старта работ.** Показ — это и есть момент
|
||||
заморозки: после него набор не двигается. **В показе называется состав по
|
||||
роду работы** — три `fix` и ни одной `feature` под целью развития это
|
||||
разговор про цель, а не про набор, и увидеть его надо до заморозки, а не в
|
||||
докладе по итогам.
|
||||
типам** — три `fix` и ни одной `feature` под целью развития это разговор про
|
||||
цель, а не про набор, и увидеть его надо до заморозки, а не в докладе по
|
||||
итогам.
|
||||
|
||||
Здесь же последний дешёвый момент заметить **разнородную задачу**: раздел
|
||||
«Затрагивает» показывает границы до того, как заведено предложение об
|
||||
@@ -200,10 +242,9 @@
|
||||
перечня, — кандидат на разрез (шов — в `tasks`, `references/split.md`).
|
||||
Замеченная здесь, она стоит одного `edit`; замеченная на ревью — выброшенного
|
||||
предложения.
|
||||
5. Задача, которой для взятия не хватает только критериев приёмки, границ или
|
||||
рода, дописывается здесь же — 2–5 утверждений с оракулами, перечень
|
||||
затрагиваемых границ, `--kind`. Но если для этого нужен ответ человека, это
|
||||
вопрос, и задача в набор не идёт.
|
||||
5. Задача, которой для взятия не хватает только разделов её типа, дописывается
|
||||
здесь же — критерии с оракулами, перечень границ, шаги воспроизведения. Но
|
||||
если для этого нужен ответ человека, это вопрос, и задача в набор не идёт.
|
||||
|
||||
**Размер — ориентир, а не закон:** 5–8 задач. Можно взять больше, можно меньше —
|
||||
набор под цель важнее круглого числа; одна крупная задача спринтом тоже бывает.
|
||||
@@ -213,9 +254,11 @@
|
||||
- Что просмотрено: N из M, сколько порций, по какому признаку отобраны.
|
||||
- Вопросы: разобрано N, из них отвечено без человека N, снято тегов N.
|
||||
- Разбор процесса: что записано и куда.
|
||||
- Сверка документов с кодом: звался ли `doc-code-drift`, что проверено из
|
||||
названного, что разошлось.
|
||||
- Изменения списком: удалено как реализованное (со ссылками), ушло без
|
||||
реализации (с причинами), понижено до идей, слито, сменило цель.
|
||||
- Новый спринт: цель, набор со слагами, дата, состав по роду работы.
|
||||
реализации (с причинами), понижено до сырья, слито, сменило тип или цель.
|
||||
- Новый спринт: цель, набор со слагами, дата, состав по типам.
|
||||
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
|
||||
цели остались — иначе доклад читается как «беклог разобран».
|
||||
- `tasks.py check` после правок — результат строкой.
|
||||
|
||||
@@ -81,6 +81,16 @@ flowchart TD
|
||||
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
|
||||
замороженный набор, который нельзя двигать, только мешает.
|
||||
|
||||
**Протухший набор — второй законный повод роспуска.** Работа стояла, и человек
|
||||
вернулся к спринту, состав которого уже не держит в голове. Тем же роспуском:
|
||||
`sprint close --dissolve --reason "работа стояла с <когда>"`, недоделанное в
|
||||
беклог, новый набор — после переоценки, а не поверх старого.
|
||||
|
||||
Порога в неделях нет и не будет: счётчик простоя пришлось бы вести руками, а
|
||||
решает всё равно человек. Признак — не срок, а **что набор перестал быть твоим**:
|
||||
взялся перечитывать, зачем эти задачи вместе, — он протух. Заморозка тут не
|
||||
мешает, она запрещает *двигать* набор, а не распустить его целиком.
|
||||
|
||||
## Определение готовности
|
||||
|
||||
Задача засчитывается сделанной, когда верно **всё**:
|
||||
@@ -95,10 +105,10 @@ flowchart TD
|
||||
отвечает «сделано по правилам», критерии — «сделано то, что заказывали».
|
||||
3. **Находки по ходу отданы списком** — исполнитель обязан их **назвать**
|
||||
(каждую, с пометкой «заведена / не заведена: причина»), но **не обязан
|
||||
заводить**: заведение интерактивно, оно требует дедупликации против беклога и
|
||||
кладбища и решений человека. Обязанность **завести урожай** — на закрытии
|
||||
спринта, ниже. Так автономный исполнитель не оказывается одновременно обязан
|
||||
завести задачи и не вправе это сделать в одиночку.
|
||||
заводить**: заведение интерактивно — оно требует дедупликации против беклога
|
||||
и кладбища, а ещё решений человека. Обязанность **завести урожай** — на
|
||||
закрытии спринта, ниже. Иначе автономный исполнитель оказался бы разом и
|
||||
обязан завести задачи, и не вправе сделать это в одиночку.
|
||||
|
||||
### Кто и когда закрывает
|
||||
|
||||
@@ -122,8 +132,8 @@ flowchart TD
|
||||
|
||||
**Само закрытие тоже коммитится, отдельным коммитом.** Удаление файла задачи и
|
||||
правка индекса — правки в рабочем дереве; пока они не в истории, `SPRINT.md`
|
||||
ничего не показывает, а `reopen` восстанавливает текст из `HEAD` в узком окне,
|
||||
которое закончится первым посторонним коммитом. Сообщение про учёт, а не про
|
||||
ничего не показывает, а `reopen` восстанавливает текст из `HEAD` в узком окне:
|
||||
его закроет первый посторонний коммит. Сообщение про учёт, а не про
|
||||
работу: `закрыта задача <slug>`.
|
||||
|
||||
**Дорога назад существует и обязана быть названа.** Человек на сессии сверил
|
||||
@@ -141,10 +151,10 @@ flowchart TD
|
||||
1. **Критерии переживают файл задачи.** Файл удаляется при закрытии, поэтому
|
||||
критерии копируются туда, где их увидит приёмщик. Куда именно — **отвечает
|
||||
пайплайн проекта, а не слот в `CLAUDE.md`**: он переносит их в `tasks.md`
|
||||
изменения на шаге заведения change. Проект без пайплайна называет своё место
|
||||
изменения, когда заводит change. Проект без пайплайна называет своё место
|
||||
сам.
|
||||
2. **Принимает человек на сессии, а не отдельный агент.** Декорреляция
|
||||
исполнителя и приёмщика в момент закрытия **снята** (решение о снятии и его
|
||||
2. **Принимает человек на сессии, а не отдельный агент.** Исполнитель и приёмщик
|
||||
в момент закрытия **не разведены** (решение о снятии и его
|
||||
цена — в `SKILL.md`, «Стимулы»). Опоры остались три: **сохранённый независимый
|
||||
отчёт ревью** (при конвейере `av-dev-pipeline` — отчёт триажа в
|
||||
`openspec/changes/archive/<id>/review/`, до архивации — `changes/<id>/review/`),
|
||||
|
||||
+197
-137
@@ -1,19 +1,19 @@
|
||||
---
|
||||
name: tasks
|
||||
description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). Заведение задачи, идеи или цели из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, мозговой штурм идеи, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Ритуал между спринтами — скилл session. Не реализует задачи — этим занимается пайплайн проекта.
|
||||
description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). У каждой записи есть тип (goal, feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. Заведение записи из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, штурм сырья, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Ритуал между спринтами — скилл session. Не реализует задачи — этим занимается пайплайн проекта.
|
||||
---
|
||||
|
||||
# Задачи
|
||||
|
||||
Задачи — каталог markdown-файлов. Одна запись = один файл `items/<slug>.md` плюс
|
||||
строка **ровно в одном** индексе. Скилл владеет **форматом и содержимым**:
|
||||
заводит, редактирует, закрывает, разбирает находки ревью, дробит, штурмует идеи.
|
||||
заводит, редактирует, закрывает, разбирает находки ревью, дробит, штурмует сырьё.
|
||||
|
||||
Чем он **не** владеет: ритуалом между спринтами (разбор вопросов → разбор
|
||||
прошедшего спринта → переоценка → выбор цели и набор) — это скилл `session`; и
|
||||
выполнением задачи — это пайплайн проекта.
|
||||
|
||||
## Пять правил, из которых всё следует
|
||||
## Шесть правил, из которых всё следует
|
||||
|
||||
Ситуация не покрыта инструкцией — решай по ним.
|
||||
|
||||
@@ -43,10 +43,20 @@ description: Ведение задач и целей как каталога mar
|
||||
4. **Порядка нет, есть цель — но цель есть не у всякой задачи.** Приоритетов,
|
||||
«повысить» и «встать раньше» нет: «что делать дальше» отвечает набор спринта,
|
||||
а между спринтами порядок не нужен никому. Цель обязательна там, где она и
|
||||
есть содержание работы, — у **новой возможности** (`kind:feature`). Починка,
|
||||
есть содержание работы, — у **новой возможности** (`feature`). Починка,
|
||||
техдолг и разведка служат работоспособности, а не направлению, и живут без
|
||||
цели законно; в набор спринта они входят помимо его цели. Придуманная им цель
|
||||
— то же враньё, от которого спасает род работы.
|
||||
— то же враньё, от которого спасает тип.
|
||||
|
||||
Единственный порядок, который в беклоге всё-таки есть, **производен от типа**,
|
||||
а не назначен человеком: **сырьё** (`research` без раздела «Вопрос») стоит в
|
||||
конце своей категории. Его не берут, и между берущимся оно каждый раз требует
|
||||
открыть файл, чтобы это понять. Раз порядок выводится, его проверяет машина —
|
||||
и приоритетом он не становится.
|
||||
5. **Тип решает, что с записью можно делать.** Тип — единственная ось и первое
|
||||
поле меты: от него зависит, какие разделы обязательны в теле, нужна ли цель,
|
||||
берётся ли запись в спринт и в каком индексе живёт её строка. Словарь закрыт;
|
||||
ни один тип не подошёл — значит, в записи их два, и её надо разделить.
|
||||
|
||||
## Раскладка
|
||||
|
||||
@@ -67,34 +77,44 @@ docs/tasks/
|
||||
подо что берут.** Цель в спринт взять нельзя, поэтому в списке берущихся ей не
|
||||
место.
|
||||
|
||||
**Четыре секции роадмапа, и первая отвечает на половину вопроса:**
|
||||
**Четыре секции роадмапа, и последняя отвечает на половину вопроса:**
|
||||
|
||||
| Секция | Англ. | Что в ней |
|
||||
| --- | --- | --- |
|
||||
| `Готово` | `Done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках |
|
||||
| `Запланировано` | `Planned` | очередь значима и обосновывается прозой рядом |
|
||||
| `Направления` | `Directions` | очереди нет, тянутся долго |
|
||||
| `Разработка` | `Tooling` | инструмент и процесс — не возможности приложения, и потому отдельно |
|
||||
| `Сопровождение` | `Operations` | чем держат проект: инструмент, процесс, эксплуатация — не возможности приложения, и потому отдельно |
|
||||
| `Готово` | `Done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках |
|
||||
|
||||
**Секции роадмапа канонические, секции беклога — нет**, и разница не в любви к
|
||||
единообразию. У каждой секции роадмапа свой смысл, в первую пишет сам `close`, и
|
||||
роадмап, названный по-своему, читался бы только своим автором. Секции беклога
|
||||
(`Ядро`, `Инфра`) смысла не несут — это полки, и остаются делом проекта.
|
||||
**Порядок тоже канонический, и `Готово` стоит последним не из скромности.**
|
||||
Достигнутое **копится**: через год этой секции больше, чем всех остальных
|
||||
вместе. Стоя первой, она отодвигает за экран ровно то, ради чего роадмап
|
||||
открывают чаще всего, — что делается сейчас и что дальше. Порядок проверяет
|
||||
`check`, переставляет `check --fix`.
|
||||
|
||||
Отсюда три правила, которые проверяет `tasks.py check`: **состав закреплён**
|
||||
**Секции роадмапа канонические, категории беклога — нет**, и разница не в любви к
|
||||
единообразию. У каждой секции роадмапа свой смысл, в достигнутое пишет сам `close`, и
|
||||
роадмап, названный по-своему, читался бы только своим автором. Категории беклога
|
||||
(`Ядро`, `Инфра`) смысла не несут — это полки домена, и остаются делом проекта.
|
||||
Отсюда и разные имена поля меты: у цели **Секция** (часть роадмапа — состояние
|
||||
очереди), у задачи **Категория** (полка, в которую она вернётся из спринта).
|
||||
|
||||
Отсюда четыре правила, которые проверяет `tasks.py check`: **состав закреплён**
|
||||
(чужая секция — ошибка, а не вольность), **все четыре обязаны быть** (нет
|
||||
секции — нет ответа на её часть вопроса), **язык один на весь индекс**.
|
||||
`--roadmap-sections` у `init` нет: выбирать нечего.
|
||||
секции — нет ответа на её часть вопроса), **язык один на весь индекс**, **порядок
|
||||
канонический**. `--roadmap-sections` у `init` нет: выбирать нечего.
|
||||
|
||||
**Заголовок секции — с прописной, после него пустая строка.** Во всех индексах
|
||||
одинаково, включая секции беклога, которые проект называет сам. Написание
|
||||
канонических секций правит `check --fix` (заодно и ссылку на секцию в мете
|
||||
файлов: имя секции принадлежит заголовку индекса, файл на неё только
|
||||
ссылается); отбивку он ставит везде.
|
||||
**Заголовок секции отбит пустой строкой с обеих сторон и написан с прописной.**
|
||||
Во всех индексах одинаково, включая категории беклога, которые проект называет сам.
|
||||
Написание канонических секций правит `check --fix` (заодно и ссылку на секцию в
|
||||
мете файлов: имя секции принадлежит заголовку индекса, файл на неё только
|
||||
ссылается); отбивку и порядок он правит везде.
|
||||
|
||||
Оговорка про `Разработка`: слово `окружение` сюда не годится — в
|
||||
Оговорка про `Сопровождение`: слово `окружение` сюда не годится — в
|
||||
`architecture.md` оно уже значит боевое окружение приложения, и одно слово в двух
|
||||
смыслах развело бы документы канона.
|
||||
смыслах развело бы документы канона. А `Разработка`, стоявшая тут раньше,
|
||||
называла слишком много: роадмап **весь** про разработку, и секция с таким именем
|
||||
не отличалась от остальных ничем.
|
||||
|
||||
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (спринт не может
|
||||
продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и
|
||||
@@ -135,10 +155,10 @@ stateDiagram-v2
|
||||
state "записи нет — реализована" as D
|
||||
state "ROADMAP.md, «умеет» — цель достигнута" as A
|
||||
|
||||
[*] --> B: add
|
||||
[*] --> B: add --type feature|fix|chore|research
|
||||
[*] --> P: add --type goal
|
||||
B --> P: edit --type goal --section
|
||||
P --> B: edit --type task --section
|
||||
P --> B: edit --type feature|fix|chore|research --section
|
||||
B --> S: sprint take
|
||||
S --> B: sprint drop --reason
|
||||
S --> D: close --implemented
|
||||
@@ -161,7 +181,7 @@ stateDiagram-v2
|
||||
|
||||
## Цели
|
||||
|
||||
**Цель — возможность приложения.** Такой же файл в `items/`, тип `[goal]`,
|
||||
**Цель — возможность приложения.** Такой же файл в `items/`, тип `goal` (🎯),
|
||||
перечисленный в `ROADMAP.md`. Формулируется ответом на вопрос **«что приложение
|
||||
будет уметь»**, а не названием области работ: не «Работа с чтением», а «Чтение
|
||||
данных клиентами»; не «Рефакторинг слияния», а «Исход слияния не зависит от
|
||||
@@ -173,15 +193,28 @@ stateDiagram-v2
|
||||
только, чтобы формулировка отвечала на «что приложение делает», а не на «какую
|
||||
часть кода мы трогаем».
|
||||
|
||||
**Что целью не является — работа над инструментом и процессом.** Сборка,
|
||||
проверки, сам этот скилл: на вопрос «что приложение будет уметь» они не
|
||||
отвечают. Им
|
||||
отведена отдельная секция роадмапа, чтобы они были видны в том же экране и при
|
||||
этом не читались как возможности продукта.
|
||||
**Целью не становится работа, которой держат проект.** Состав перечислен
|
||||
[в каноне](../canon/references/canon.md), раздел «Сопровождение и эксплуатация»;
|
||||
на вопрос «что приложение будет уметь» ничто из него не отвечает. Им отведена отдельная секция роадмапа,
|
||||
чтобы они были видны в том же экране и при этом не читались как возможности
|
||||
продукта.
|
||||
|
||||
**Граница проходит по тому, кто наблюдает, а не по теме.** «Приложение сообщает
|
||||
о своём состоянии» — возможность: наблюдает пользователь сервиса, и цели место
|
||||
среди прочих. «Дежурный видит состояние на одном экране» — сопровождение:
|
||||
наблюдаем мы. Одна и та же наблюдаемость попадает в разные секции, и это верно —
|
||||
секции отвечают на разные вопросы.
|
||||
|
||||
**Сопровождение и эксплуатация — целое и часть**, а не синонимы, и та же тема
|
||||
живёт ещё в двух местах канона: разделе «Эксплуатация» в `architecture.md` и
|
||||
эксплуатационном проходе ревью. Словарь у всех трёх общий и живёт одним домом —
|
||||
[canon.md](../canon/references/canon.md), раздел «Сопровождение и эксплуатация».
|
||||
Пересказывать его здесь нельзя: три перечня «чем держат проект» уже разъезжались
|
||||
на «метриках и логах» против «мониторинга».
|
||||
|
||||
Секция выбирается так: очередь значима и обоснована прозой — `Запланировано`;
|
||||
тянется долго и очереди не имеет — `Направления`; не про приложение —
|
||||
`Разработка`; в `Готово` кладёт сам `close`.
|
||||
тянется долго и очереди не имеет — `Направления`; не про приложение, а про то,
|
||||
чем его держат, — `Сопровождение`; в `Готово` кладёт сам `close`.
|
||||
|
||||
- **Список задач цели выводится, а не хранится.** В теле цели — зачем она и что
|
||||
считается её завершением; перечня задач там нет. Он был бы третьим индексом и
|
||||
@@ -205,52 +238,51 @@ stateDiagram-v2
|
||||
проектов. Встретился в чужом беклоге — это цель либо набор задач, и `check`
|
||||
назовёт его неизвестным типом.
|
||||
|
||||
## Род работы
|
||||
## Тип записи
|
||||
|
||||
**Тип записи и род работы — две оси, и путать их нельзя.** Тип отвечает «что это
|
||||
за запись» (цель, идея, задача), род — «какого рода работа»: `feature`, `fix`,
|
||||
`chore`, `research`. Одним значением на оба вопроса не ответить: идея бывает
|
||||
*про* функцию, а цель функцией *и является*.
|
||||
**Тип — единственная ось, и он решает, что с записью можно делать.** Дом типа —
|
||||
**поле меты `Тип` первой строкой**; эмодзи в заголовке H1 от него производна, её
|
||||
ставит `add` и чинит `check --fix`.
|
||||
|
||||
- **`feature`** — снаружи появляется или меняется то, чего раньше не было.
|
||||
- **`fix`** — поведение расходится с заявленным, и расхождение воспроизводится.
|
||||
Не воспроизводится — это `research`, а не `fix`.
|
||||
- **`chore`** — обслуживание: зависимости, сборка, перенос, чистка. Наблюдаемое
|
||||
поведение не меняется, и в этом всё дело: **у `chore` тест готовности слабее
|
||||
честно**, а не молча. «Что станет наблюдаемо иначе» здесь отвечается
|
||||
разработчику («перестанет собираться два раза», «уедет последний вызов
|
||||
устаревшего API»), а не пользователю. Пока рода не было, такие задачи либо не
|
||||
заводились, либо формулировались как выдуманная польза.
|
||||
- **`research`** — исход работы знание, а не изменение системы: ответ на вопрос,
|
||||
замер, разведка. Приёмка — записанный ответ (`docs/research/`, ADR, тело
|
||||
задачи), а не изменённый код.
|
||||
| Тип | Обязательные разделы | Цель | В спринт | Устав |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 🎯 `goal` | `Завершение` | — | нет | [task-goal.md](references/task-goal.md) |
|
||||
| ✨ `feature` | `Затрагивает`, `Критерии приёмки` | **обязательна** | да | [task-feature.md](references/task-feature.md) |
|
||||
| 🐞 `fix` | `Воспроизведение`, `Затрагивает`, `Критерии приёмки` | необязательна | да | [task-fix.md](references/task-fix.md) |
|
||||
| 🧹 `chore` | `Затрагивает`, `Критерии приёмки` | нет | да | [task-chore.md](references/task-chore.md) |
|
||||
| 🔬 `research` | `Вопрос`, `Куда ляжет ответ` | нет | да | [task-research.md](references/task-research.md) |
|
||||
|
||||
Дом рода — **тег `kind:<род>`**, а не префикс заголовка и не поле меты: теги
|
||||
здесь единственный механизм разметки, и `list --kind fix` работает даром. Цена
|
||||
известна: в строку индекса род не попадает (индексы производны), и «в наборе одни
|
||||
починки» видно командой, а не глазами по `SPRINT.md`.
|
||||
Сверх обязательных у любой задачи допустимы `Рамки` и `Вопросы`. Раздел не из
|
||||
схемы своего типа — **замечание, а не ошибка**: свой раздел законная вольность
|
||||
проекта, но `Воспроизведение` у `chore` почти всегда значит, что тип проставлен
|
||||
не тот, и сказать об этом стоит, не запрещая.
|
||||
|
||||
**Осей было две, и ортогональность у них была фальшивой.** Тип записи
|
||||
(`goal`/`idea`/`task`) и род работы (`kind:<род>` тегом) давали двенадцать клеток
|
||||
произведения, из которых законны были шесть: у цели род запрещён, у задачи
|
||||
обязателен, у идеи пуст. Плюс алгоритм работы крепится не к `task`, а к `fix` и
|
||||
`research` — то есть к роду. Оси схлопнуты, тег `kind:` упразднён.
|
||||
|
||||
**Тип `idea` упразднён вместе с ними.** Он значил не род работы, а **состояние
|
||||
незаполненности** — «первый, второй или третий вопрос теста готовности не
|
||||
отвечается», — а состояние типом быть не может: оно меняется по мере того, как
|
||||
запись дописывают, а тип меняют командой. Теперь это состояние называется честно:
|
||||
`research` без раздела «Вопрос» — **сырьё**. В спринт не берётся ровно как
|
||||
прежняя идея, лежит в конце своей категории и отбирается `list --raw`.
|
||||
|
||||
Словарь **закрыт**. Открытый разъедется на синонимах — `bug`, `bugfix`, `fix`,
|
||||
`defect`, — и отбор по роду перестанет отвечать на свой единственный вопрос. Ни
|
||||
один род не подходит — это сигнал, что в задаче их два и её надо разделить.
|
||||
`defect`, — и отбор по типу перестанет отвечать на свой единственный вопрос. Ни
|
||||
один тип не подходит — это сигнал, что в задаче их два и её надо разделить.
|
||||
|
||||
**Род обязателен у задачи, у цели запрещён, у идеи необязателен** — идея получает
|
||||
его, когда становится задачей. Требуется он там, где по нему принимают решение:
|
||||
`sprint take` без рода откажет. `check` о пропаже только **напоминает** — беклог,
|
||||
заведённый до появления рода, законен, и переоформлять его «заодно» здесь не
|
||||
просят.
|
||||
**Требуется тип там, где по нему принимают решение:** `sprint take` без типа
|
||||
откажет, потому что не знает, каких разделов требовать. `check` о пропаже только
|
||||
**напоминает** — беклог, заведённый до появления типа, законен, и переоформлять
|
||||
его «заодно» здесь не просят.
|
||||
|
||||
**Род решает и то, обязательна ли цель.** `feature` без цели не бывает: новая
|
||||
возможность и есть содержание цели, и если подходящей нет — либо она заводится,
|
||||
либо это не `feature`. `fix`, `chore` и `research` живут без цели законно, и
|
||||
`check` о них молчит: они служат работоспособности, а не направлению. Это
|
||||
единственный случай, когда род что-то определяет за пределами отбора, — и
|
||||
определяет он учёт, а не процесс проверки.
|
||||
|
||||
**Род не выбирает профиль ревью и вообще ничего не предписывает пайплайну.**
|
||||
Профиль выбирается по факту изменения, а не по роду задачи: `chore` бывает
|
||||
**Тип не выбирает профиль ревью и вообще ничего не предписывает пайплайну.**
|
||||
Профиль выбирается по факту изменения, а не по типу задачи: `chore` бывает
|
||||
миграцией схемы, `fix` — правкой публичного контракта. Правило «предписание
|
||||
процесса в теле задачи снимается» родом не отменяется, а подтверждается: он
|
||||
процесса в теле задачи снимается» типом не отменяется, а подтверждается: он
|
||||
описывает работу, а не то, как её проверять.
|
||||
|
||||
## Как написана задача
|
||||
@@ -262,17 +294,18 @@ stateDiagram-v2
|
||||
|
||||
| Тип | Отвечает на | Пример |
|
||||
| --- | --- | --- |
|
||||
| цель | что приложение будет уметь | Соперником может быть компьютер |
|
||||
| задача | что нужно сделать | Печатать поле одним куском кода |
|
||||
| идея | о чём она | Подсказка следующего хода |
|
||||
| 🎯 `goal` | что приложение будет уметь | Соперником может быть компьютер |
|
||||
| ✨ `feature`, 🐞 `fix`, 🧹 `chore` | что нужно сделать | Печатать поле одним куском кода |
|
||||
| 🔬 `research` | о чём разведка | Подсказка следующего хода |
|
||||
|
||||
Задача — **глаголом в неопределённой форме**, перед ним допускается «не»: «Не
|
||||
отбрасывать молча лишние символы в ходе», а не «Лишние символы молча
|
||||
отбрасываются». Описательный заголовок называет **состояние**, а из состояния не
|
||||
видно, чего от работы ждут: «Ничья объявляется, пока клетки есть» одинаково
|
||||
читается и как жалоба, и как задание, — и в списке, где решают «брать или не
|
||||
брать», это разные вещи. Идея формы действия не несёт **намеренно**: что делать,
|
||||
ещё неизвестно, и заголовок-действие обещал бы решённость, которой нет.
|
||||
брать», это разные вещи. `research` формы действия не несёт **намеренно**: её
|
||||
исход знание, а что делать — ещё неизвестно, и заголовок-действие обещал бы
|
||||
решённость, которой нет.
|
||||
|
||||
Из этого же правила растёт разница индексов: роадмап — список возможностей,
|
||||
беклог — список работ, и если заголовки перепутать формами, каждый из них
|
||||
@@ -282,7 +315,7 @@ stateDiagram-v2
|
||||
здоровья, не замечанием на файл: проверка эвристическая (первое слово на
|
||||
`-ть`/`-ти`/`-чь`), а беклог, заведённый до правила, не переоформляют «заодно».
|
||||
Годность формулировки — не машине: её смотрит
|
||||
[агент вычитки](#вычитка-формулировок).
|
||||
[агент вычитки](#вычитка-два-прохода-а-не-один).
|
||||
|
||||
**Функции и границы, а не намерения.** Задача называет, что система начнёт
|
||||
делать, и какие границы это трогает: эндпоинт или команду, таблицу и миграцию,
|
||||
@@ -315,7 +348,7 @@ stateDiagram-v2
|
||||
|
||||
И одно требование, которое есть только у задачи: **сложность формулировки — не
|
||||
признак сложности работы.** Задачу, которую не удаётся сказать просто, чаще
|
||||
всего не удаётся и оценить: это либо две задачи, либо идея.
|
||||
всего не удаётся и оценить: это либо две задачи, либо сырьё.
|
||||
|
||||
Эти правила — про **язык**, а не про объём: короткая задача без границ хуже
|
||||
длинной с ними.
|
||||
@@ -328,16 +361,16 @@ stateDiagram-v2
|
||||
|
||||
```
|
||||
python3 $tk check --dir D # согласованность индексов + здоровье
|
||||
python3 $tk check --dir D --fix # + починить дрейф (секция, заголовок, дубли, «зачем», форма меты)
|
||||
python3 $tk list --dir D [--stale] [--section S] [--type T] [--kind K] [--tag a,b] [--goal S] [--index …] [--questions]
|
||||
python3 $tk add --dir D --slug S --title T [--type goal|idea] [--section S] [--goal G] [--kind K] [--why «зачем»] [--tag a,b]
|
||||
python3 $tk edit S --dir D [--title T] [--why «зачем»] [--type T] [--goal G] [--kind K] [--add-tag a,b] [--rm-tag c]
|
||||
python3 $tk check --dir D --fix # + починить дрейф (тип, эмодзи, место, заголовок, дубли, «зачем», форма меты)
|
||||
python3 $tk list --dir D [--stale] [--section S] [--type T] [--tag a,b] [--goal S] [--raw] [--index …] [--questions]
|
||||
python3 $tk add --dir D --slug S --title T --type goal|feature|fix|chore|research [--section S] [--goal G] [--why «зачем»] [--tag a,b]
|
||||
python3 $tk edit S --dir D [--title T] [--why «зачем»] [--type T] [--goal G] [--add-tag a,b] [--rm-tag c]
|
||||
python3 $tk move S --dir D --section S [--reason R] [--after S | --first]
|
||||
python3 $tk close S --dir D --reason R # в REJECTED.md + удалить (ушла без реализации)
|
||||
python3 $tk close S --dir D --implemented # просто удалить (реализована и закоммичена)
|
||||
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 init --dir D [--sections …] [--roadmap-sections …] [--items …] [--backlog …] …
|
||||
python3 $tk init --dir D [--sections …] [--items …] [--backlog …] …
|
||||
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md
|
||||
```
|
||||
|
||||
@@ -354,20 +387,22 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
|
||||
нет» — нерабочая, и одинаковая реакция на них была бы неверна в обоих случаях.
|
||||
|
||||
Тип — английское ключевое слово `goal` / `idea` / `task` (как и прочие токены
|
||||
команд); `task` префикса не несёт, остальные кодируются `[goal]`/`[idea]` в
|
||||
заголовке. Текст задачи при этом русский.
|
||||
Тип — английское ключевое слово `goal` / `feature` / `fix` / `chore` /
|
||||
`research` (как и прочие токены команд), у `add` **обязательное**: без него
|
||||
неизвестно, какой шаблон тела класть. Текст задачи при этом русский, а эмодзи в
|
||||
заголовке ставит скрипт.
|
||||
|
||||
**Мутации правят файл и индексы заодно** — руками строку индекса или мету
|
||||
не пиши, зови `add`/`edit`/`move`/`close`/`sprint`. Смена заголовка, «зачем», типа,
|
||||
цели, рода работы и **тегов** — это `edit`: он держит H1, мету и индекс в синхроне.
|
||||
Снятие тега — `--rm-tag` (после ответа на вопрос снимается `question`), смена
|
||||
цели — `--goal`, рода — `--kind`; оба заменяют прежнее значение, а не добавляют
|
||||
второе.
|
||||
цели и **тегов** — это `edit`: он держит H1 (вместе с эмодзи), мету и индекс
|
||||
согласованными. Снятие тега — `--rm-tag` (после ответа на вопрос снимается
|
||||
`question`), смена цели — `--goal`, типа — `--type`; оба заменяют прежнее
|
||||
значение, а не добавляют второе.
|
||||
|
||||
**Переезд между индексами — следствие смены типа, а не отдельная команда.**
|
||||
`edit <slug> --type goal --section <часть роадмапа>` переносит строку из
|
||||
`BACKLOG.md` в `ROADMAP.md` (и обратно `--type task --section <секция беклога>`);
|
||||
`BACKLOG.md` в `ROADMAP.md` (и обратно — задачным типом плюс
|
||||
`--section <категория беклога>`);
|
||||
`move` двигает только внутри одного индекса и пишет причину. `--section` у
|
||||
`edit` работает **только** при таком переезде — иначе он отсылает к `move`,
|
||||
потому что смена секции без причины и есть тот дрейф, который потом никто не
|
||||
@@ -380,39 +415,54 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
`check` — единственный судья согласованности; что именно он ловит, скажет его
|
||||
вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой
|
||||
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившееся
|
||||
чини `check --fix` — он детерминированно правит то, где истина однозначна
|
||||
(секция, заголовок, дубли, «зачем» из индекса в файл, старая форма меты,
|
||||
пометка `decomposed` у цели с задачами), а неоднозначное (задача сразу в двух
|
||||
индексах, нечего восстанавливать) печатает отдельной пометкой `НЕОДНОЗНАЧНО` —
|
||||
это тебе, и это идёт строкой доклада. **Ссылка на исчезнувший файл в пометку не
|
||||
попадает:** `--fix` её просто не трогает, и она остаётся `ОШИБКА` обычного
|
||||
`check` — то есть видна, но в докладе её надо назвать отдельно.
|
||||
чини `check --fix` — он детерминированно правит то, где истина однозначна (тип в
|
||||
своё поле, эмодзи заголовка, имя поля места, секция, заголовок, дубли, «зачем» из
|
||||
индекса в файл, старая форма меты, пометка `decomposed` у цели с задачами, сырьё
|
||||
в конец категории), а неоднозначное (задача сразу в двух индексах, нечего
|
||||
восстанавливать, **тип, которого неоткуда взять**) печатает отдельной пометкой
|
||||
`НЕОДНОЗНАЧНО` — это тебе, и это идёт строкой доклада. **Ссылка на исчезнувший
|
||||
файл в пометку не попадает:** `--fix` её просто не трогает, и она остаётся
|
||||
`ОШИБКА` обычного `check` — то есть видна, но в докладе её надо назвать отдельно.
|
||||
|
||||
`--fix` правит **и файлы** — ровно в двух местах, где источник ровно один и
|
||||
выбирать не из чего: «зачем», оставшееся только в индексе, переезжает в мету,
|
||||
и цель, у которой есть задачи, получает тег `decomposed`. Оба случая печатаются
|
||||
поимённо.
|
||||
`--fix` правит **и файлы** — там, где источник ровно один и выбирать не из чего:
|
||||
тип переезжает из прежнего дома (тег `kind:`, префикс `[goal]`/`[idea]`) в поле
|
||||
меты, заголовок получает эмодзи, поле места — имя по типу, «зачем», оставшееся
|
||||
только в индексе, переезжает в мету, цель с задачами получает `decomposed`.
|
||||
Каждый случай печатается поимённо.
|
||||
|
||||
**Тип, который не выводится ниоткуда, `--fix` не угадывает.** `feature` от
|
||||
`chore` машина не отличает, и подставленное наугад значение врало бы ровно там,
|
||||
где по нему принимают решение. Такие записи идут в `НЕОДНОЗНАЧНО`, и тип им
|
||||
проставляет человек — `edit <слаг> --type …`.
|
||||
|
||||
**Что механизировано, а что нет.** У задачи, взятой в набор (`sprint take` и
|
||||
`check` по задачам спринта), проверяются три вещи, и у каждой своя глубина:
|
||||
`check` по задачам спринта), проверяется схема её типа, и у каждой части своя
|
||||
глубина:
|
||||
|
||||
- **критерии приёмки** — число пунктов жёстко (меньше двух отказ, больше пяти
|
||||
замечание), наличие оракула **эвристикой** по слову «оракул» в пункте;
|
||||
- **род работы** — жёстко: назван и из закрытого словаря;
|
||||
- **раздел «Затрагивает»** — только **наличие непустого**. Полнота перечня машине
|
||||
не видна: границу, которую забыли назвать, она от отсутствующей не отличает.
|
||||
- **тип** — жёстко: назван и из закрытого словаря;
|
||||
- **критерии приёмки** (`feature`, `fix`, `chore`) — число пунктов жёстко
|
||||
(меньше двух отказ, больше пяти замечание), наличие оракула **эвристикой** по
|
||||
слову «оракул» в пункте;
|
||||
- **прочие разделы схемы** (`Затрагивает`, `Воспроизведение`, `Вопрос`,
|
||||
`Куда ляжет ответ`, `Завершение`) — только **наличие непустого**. Содержимое
|
||||
машине не видно: границу, которую забыли назвать, она от отсутствующей не
|
||||
отличает, а шаги, по которым ничего не воспроизводится, — от годных.
|
||||
|
||||
Настоящий оракул от слова «оракул» машина тоже не отличает, поэтому эвристика
|
||||
даёт только замечание, и в докладе это называется как есть: «проверено число
|
||||
пунктов и наличие границ, годность оракулов и полнота границ — глазами».
|
||||
даёт только замечание, и в докладе это называется как есть: «проверено наличие
|
||||
разделов своего типа и число критериев, годность оракулов и полнота границ —
|
||||
глазами».
|
||||
|
||||
Формат файла, меты, слага, индексов и `REJECTED.md` —
|
||||
[references/task-format.md](references/task-format.md). Там же тест «готова к
|
||||
взятию», требования к критериям приёмки и раздел «Затрагивает».
|
||||
Формат записи, меты, слага, индексов и `REJECTED.md` —
|
||||
[references/task-format.md](references/task-format.md); там же тест «готова к
|
||||
взятию». Схема и алгоритм каждого типа — по файлу на тип:
|
||||
[goal](references/task-goal.md) · [feature](references/task-feature.md) ·
|
||||
[fix](references/task-fix.md) · [chore](references/task-chore.md) ·
|
||||
[research](references/task-research.md).
|
||||
|
||||
## Сценарии
|
||||
|
||||
### Завести задачу, идею или цель из диалога
|
||||
### Завести запись из диалога
|
||||
|
||||
1. **Фильтр.** Делаем прямо сейчас — не заводим. Не пожалеем о потере — не
|
||||
заводим. Родилось три кандидата — покажи их и спроси, какие заводить: молча
|
||||
@@ -423,30 +473,36 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
ту строку и что изменилось с момента отказа (`add` предупредит и сам, но
|
||||
молча заводить нельзя). Две задачи об одном — самая дорогая находка
|
||||
переоценки.
|
||||
3. **Тип по тесту готовности** (см. task-format): проходит — задача, не
|
||||
проходит — идея (`--type idea`). Не делается одним заходом — это не эпик, а
|
||||
несколько задач под одной целью: дроби сразу. Возможность приложения, а не
|
||||
шаг — цель (`--type goal`).
|
||||
4. **Цель задачи — если род её требует.** У `feature` должен быть
|
||||
`--goal <слаг>`: новая возможность и есть содержание цели. Подходящей нет —
|
||||
либо она заводится (`--type goal`), либо перед тобой не `feature`. У `fix`,
|
||||
`chore` и `research` цели может не быть вовсе, и придумывать её не надо. У
|
||||
идеи цель проставляется, когда идея становится задачей.
|
||||
5. **Род работы** — `--kind feature|fix|chore|research` (см. «Род работы»). Не
|
||||
подходит ни один — задача не одна, разбирай.
|
||||
6. `add …`, затем допиши тело редактором: одна фраза, **затрагиваемые границы**,
|
||||
критерии приёмки с оракулами, рамки. «Зачем» отвечает «зачем нужна эта
|
||||
задача» — состояние, остаток, боль, — а не пересказывает первый абзац, и
|
||||
пишется **для человека**: не «канонизация внутри транзакции», а «тело 40 МиБ
|
||||
держит блокировку 5 секунд, соседние доставки уходят в отказ».
|
||||
7. `check`.
|
||||
3. **Тип** — `--type` обязателен, и он же первое содержательное решение:
|
||||
|
||||
- возможность приложения, а не шаг к ней → `goal`;
|
||||
- снаружи появляется то, чего не было → `feature`;
|
||||
- поведение расходится с заявленным и **воспроизводится** → `fix`
|
||||
(не воспроизводится → `research`);
|
||||
- обслуживание, наблюдаемое поведение не меняется → `chore`;
|
||||
- исход — знание, а не изменение системы → `research`.
|
||||
|
||||
Не подходит ни один — в записи их два, разбирай. Не проходит тест готовности
|
||||
(см. task-format) — это **сырьё**: `--type research`, раздел «Вопрос» пока
|
||||
пуст, место в конце категории. Не делается одним заходом — это не эпик, а
|
||||
несколько задач под одной целью: дроби сразу.
|
||||
4. **Цель — если тип её требует.** У `feature` должен быть `--goal <слаг>`:
|
||||
новая возможность и есть содержание цели. Подходящей нет — либо она
|
||||
заводится (`--type goal`), либо перед тобой не `feature`. У `fix`, `chore` и
|
||||
`research` цели может не быть вовсе, и придумывать её не надо.
|
||||
5. `add …`, затем допиши тело редактором **по схеме своего типа** — шаблон её
|
||||
уже разложил, устав типа объясняет каждый раздел. «Зачем» отвечает «зачем
|
||||
нужна эта задача» — состояние, остаток, боль, — а не пересказывает первый
|
||||
абзац, и пишется **для человека**: не «канонизация внутри транзакции», а
|
||||
«тело 40 МиБ держит блокировку 5 секунд, соседние доставки уходят в отказ».
|
||||
6. `check`.
|
||||
|
||||
### Разобрать находки аудита или ревью
|
||||
|
||||
Ревью и аудиты — тоже источник задач, но с зеркальной диалогу опасностью: не
|
||||
Ревью и аудиты — тоже источник задач, но с опасностью, зеркальной диалогу: не
|
||||
пять файлов из одной мысли, а сорок файлов из сорока сырых находок. Защита та
|
||||
же, что в самом ревью: кластеризация по причине, дедуп против живых и
|
||||
`REJECTED.md`, находка без свидетельства → идея, а не задача, и карта кластеров
|
||||
`REJECTED.md`, находка без свидетельства → сырьё (`research`), а не задача, и карта кластеров
|
||||
пользователю до создания файлов. Порядок, отображение серьёзности и привязка к
|
||||
целям — [references/from-review.md](references/from-review.md).
|
||||
|
||||
@@ -460,7 +516,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
Если переводить надо не только задачи, а весь `docs/` — это скилл
|
||||
`av-dev-pm:canon`, и он зовёт этот сценарий сам на своём шаге.
|
||||
|
||||
### Декомпозиция и штурм идеи
|
||||
### Декомпозиция и штурм сырья
|
||||
|
||||
[references/split.md](references/split.md). Обе операции превращают одну запись в
|
||||
несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и
|
||||
@@ -530,10 +586,14 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
- **предписание процесса в теле** — «делать таким-то профилем ревью», «взять
|
||||
такой-то агент»: это второй дом для правила выбора и путь понизить требования
|
||||
решением, принятым до проектирования. Снимается;
|
||||
- **род, разошедшийся с задачей** — задача заводилась починкой, а после разбора
|
||||
- **тип, разошедшийся с задачей** — задача заводилась починкой, а после разбора
|
||||
оказалось, что поведение никогда и не было заявлено: это `feature`, а не `fix`.
|
||||
Правится `edit <slug> --kind …`; род, оставшийся от прошлой формулировки, врёт
|
||||
ровно там, где по нему отбирают;
|
||||
Правится `edit <slug> --type …`; тип, оставшийся от прошлой формулировки, врёт
|
||||
ровно там, где по нему отбирают, **и требует не тех разделов**: у брошенного
|
||||
`fix` останется «Воспроизведение», которого нечем заполнить;
|
||||
- **сырьё, у которого появился вопрос** — разведка обросла формулировкой, но
|
||||
раздел «Вопрос» так и пуст: она числится сырьём и в спринт не берётся.
|
||||
Записывается вопрос, и `check --fix` поднимает строку из конца категории;
|
||||
- **границы, названные вместо реализации** — «переписать хранилище на новый
|
||||
драйвер» в разделе «Затрагивает» это не граница, а замысел. Границы —
|
||||
`таблица points и её миграция`, `эндпоинт POST /ingest`, `формат отпечатка на
|
||||
@@ -598,7 +658,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
|
||||
- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным
|
||||
предварительным суждением (**рекомендация — первым вариантом**). Что выкинуть,
|
||||
под какую цель отнести, какая рамка идеи верна — решение пользователя. Слаг,
|
||||
под какую цель отнести, какая рамка разведки верна — решение пользователя. Слаг,
|
||||
формулировка, порядок строк в индексе — механика, делаем сами.
|
||||
- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа;
|
||||
решений больше — веди **несколько итераций** диалога по ≤3, а не один
|
||||
|
||||
@@ -53,8 +53,9 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
- **английские слаги.** Перевести `taj-brejk-pri-ravnoj-polnote` в
|
||||
`tie-break-equal-completeness` может только тот, кто понимает смысл. `scan`
|
||||
честно говорит: проверить надо **все** слаги, признаки транслита — эвристика;
|
||||
- **цели.** Шаги роадмапа — готовые цели из **«порядка»** (очередь и обоснование у
|
||||
них уже есть); тематические скопления задач — **«темы»** («прочность слияния»,
|
||||
- **цели.** Шаги роадмапа — готовые цели в **`Запланировано`** (очередь и
|
||||
обоснование у них уже есть); тематические скопления задач — цели в
|
||||
**`Направления`** («прочность слияния»,
|
||||
«журнал и пересборка»). Предлагаешь ты, назначает человек;
|
||||
- **что вообще не задача.** Обоснование порядка шагов, абзац прозой, заголовок
|
||||
раздела — это не пункты беклога, и они уходят в «не разложилось» с причиной.
|
||||
@@ -70,7 +71,8 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
прохода дадут два несогласованных состояния.
|
||||
3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи;
|
||||
список `goals` — из шагов роадмапа и из тем. Закрытый шаг целью не
|
||||
заводится. Пустой `goal` — законный исход только у идеи.
|
||||
заводится. Пустой `goal` законен у `fix`, `chore` и `research` — они служат
|
||||
работоспособности, а не направлению; у `feature` цель обязательна.
|
||||
4. **Покажи человеку карту** через `AskUserQuestion`, ≤3 вопроса за итерацию,
|
||||
рекомендация первым вариантом. Показывается: сколько записей, предлагаемые
|
||||
цели (порядок и темы) с обоснованием, спорные отнесения, список «не
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
## Находка агента — не задача
|
||||
|
||||
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
|
||||
воспроизводимый шаг, положение гайда). Согласие нескольких находок само по себе
|
||||
воспроизводимый шаг, положение руководства). Согласие нескольких находок само по себе
|
||||
достоверность не повышает: это один источник, высказавшийся несколько раз.
|
||||
|
||||
Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»:
|
||||
@@ -23,8 +23,11 @@
|
||||
- **Находка со свидетельством**, отложенная к исполнению → **задача**.
|
||||
Свидетельство и последствие переносим в тело — это её «почему», то самое, что
|
||||
переживает запись.
|
||||
- **Находка без свидетельства / низкой уверенности** → **идея** (`[idea]`), а не
|
||||
задача. Её судьба — штурм, где либо найдётся подтверждение, либо она уедет в
|
||||
- **Находка без свидетельства / низкой уверенности** → **сырьё**: `research`, у
|
||||
которого раздел «Вопрос» и есть недостающее свидетельство («при каких условиях
|
||||
это воспроизводится»). Не `fix`: без `Воспроизведения` его в спринт не
|
||||
возьмут, и правильно — чинить нечего, пока непонятно, что ломается. Судьба
|
||||
сырья — штурм, где либо найдётся подтверждение, либо оно уедет в
|
||||
`REJECTED.md`.
|
||||
- **Уже починено по ходу ревью** → **ничего**. Починенное не заводим.
|
||||
- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с
|
||||
@@ -44,13 +47,13 @@
|
||||
4. **Разложи по целям — там, где цель нужна.** Большинство находок ревью это
|
||||
`fix` и `chore`, и **цель им не требуется**: они служат работоспособности, а
|
||||
не направлению, и в спринт входят помимо его цели. Придуманная им цель —
|
||||
ровно то враньё, от которого спасает род работы.
|
||||
ровно то враньё, от которого спасает тип.
|
||||
|
||||
Цель обязательна у находки, которая оказалась **новой возможностью**
|
||||
(`kind:feature`): нашлось поведение, которого никто не заказывал, и его надо
|
||||
(`feature`): нашлось поведение, которого никто не заказывал, и его надо
|
||||
либо заказать целью, либо убрать. Подходящей цели нет — заведи её
|
||||
(`add --type goal --section Направления`) в том же проходе.
|
||||
5. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в
|
||||
5. **Покажи карту до создания файлов.** Кластер → задача / сырьё / строка в
|
||||
пакетный файл / уже заведено / отброшено, и под какую цель — пачкой через
|
||||
`AskUserQuestion`. Это тот же барьер, что и «три кандидата» в интейке из
|
||||
диалога: массовое заведение файлов без подтверждения — ровно тот отказ, ради
|
||||
@@ -58,13 +61,14 @@
|
||||
заводиться и без поштучного вопроса — но карта пользователю предъявляется
|
||||
всё равно.
|
||||
6. **Заводи утверждённое** через `tasks.py add`, с тремя добавками:
|
||||
- **тег партии** — `--tag review-ГГГГ-ММ-ДД` (или `audit-<тема>`), чтобы весь
|
||||
- **тег партии** — `--tag review-ГГГГ-ММ-ДД` (или `audit-<slug>`), чтобы весь
|
||||
заход разбора поднимался одной командой `list --tag …`;
|
||||
- **род работы** — `--kind`. У находок ревью он **не по умолчанию `fix`**:
|
||||
починкой считается расхождение с заявленным поведением, а находка «этого
|
||||
свойства никто не заказывал» — это `feature`, находка «не знаем, как
|
||||
поведёт себя драйвер» — `research`. Род, розданный оптом, врёт ровно там,
|
||||
где по нему потом отбирают;
|
||||
- **тип** — `--type`, и он **не по умолчанию `fix`**: починкой считается
|
||||
расхождение с заявленным поведением, а находка «этого свойства никто не
|
||||
заказывал» — это `feature`, находка «не знаем, как поведёт себя драйвер» —
|
||||
`research`. Тип, розданный оптом, врёт ровно там, где по нему потом
|
||||
отбирают, **и требует не тех разделов**: каждому `fix` придётся заполнить
|
||||
`Воспроизведение`, а у находки без свидетельства его нет;
|
||||
- **провенанс в теле** — кто нашёл, каким проходом, с каким свидетельством.
|
||||
Без него через месяц не отличить проверенную находку от догадки.
|
||||
7. `tasks.py check`.
|
||||
@@ -82,7 +86,8 @@
|
||||
- **находка, ломающая уже идущий спринт**, — не интейк вовсе: см. правило
|
||||
вторжения в скилле `session`. В беклог она падает, только если врываться не
|
||||
положено;
|
||||
- **низкая уверенность или нет свидетельства** → идея;
|
||||
- **низкая уверенность или нет свидетельства** → сырьё (`research` с пустым
|
||||
разделом «Вопрос»);
|
||||
- **мелочь** → строка в пакетный файл;
|
||||
- **уже починено / развилка решена сейчас** → ничего.
|
||||
|
||||
@@ -104,7 +109,7 @@
|
||||
|
||||
- Источник (какое ревью/аудит, сколько находок на входе).
|
||||
- Свёрнуто в задачи: N кластеров из M находок, со слагами, целями и тегом партии.
|
||||
- Что не заведено и почему: починено инлайн, уже заведено, ушло в идеи, в
|
||||
- Что не заведено и почему: починено инлайн, уже заведено, стало сырьём, ушло в
|
||||
`REJECTED.md`.
|
||||
- Поимённая сверка: находок на входе N, исход есть у N.
|
||||
- `tasks.py check`.
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на
|
||||
наследников, а не археологией git;
|
||||
- родитель осмыслен как **возможность**, а не как шаг → это цель. Тип на месте
|
||||
не меняется (цель живёт в другом индексе): заводится `[goal]` в `ROADMAP.md`,
|
||||
не меняется (цель живёт в другом индексе): заводится `--type goal` в `ROADMAP.md`,
|
||||
части получают `--goal <новый слаг>`, родитель закрывается с причиной-ссылкой.
|
||||
|
||||
**Промежуточного зонтика между целью и задачей нет.** Тип `[epic]` упразднён:
|
||||
**Промежуточного зонтика между целью и задачей нет.** Тип `epic` упразднён:
|
||||
роль зонтика играет цель, а слишком крупный шаг дробится на шаги помельче под
|
||||
той же целью. Если частям нужен общий заголовок — значит у них общая
|
||||
возможность, и её надо назвать целью, а не заводить временный тип.
|
||||
@@ -73,10 +73,15 @@
|
||||
спринт продолжается остальными. Части заводятся сразу под той же целью, но в
|
||||
текущий набор **не добавляются** — набор заморожен.
|
||||
|
||||
## Мозговой штурм идеи
|
||||
## Мозговой штурм сырья
|
||||
|
||||
Идея (`[idea]`) не проходит тест «готова к взятию»: неясно, что именно делаем.
|
||||
Штурм проясняет — и это **generative-операция, а не applicative**.
|
||||
Сырьё — запись типа `research`, у которой раздел «Вопрос» пуст: она не проходит
|
||||
тест «готова к взятию», потому что неясно, что именно делаем. Штурм проясняет —
|
||||
и это **generative-операция, а не applicative**.
|
||||
|
||||
Исход штурма и есть заполненный «Вопрос» (тогда разведку можно брать в спринт)
|
||||
или набор задач с типами, которые из ответа следуют. Третий законный исход —
|
||||
`close --reason`.
|
||||
|
||||
Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное:
|
||||
перечисляется то, что уже видно в формулировке. Ценное — на уровень выше.
|
||||
@@ -87,9 +92,9 @@ Applicative-штурм («перечисли задачи, следующие и
|
||||
applicative.
|
||||
2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку
|
||||
выбирает он: это продуктовое решение, не механика.
|
||||
3. **Назови цель.** Выбранная форма служит какой-то цели — существующей или
|
||||
новой. Идея, для которой цель не находится, скорее всего уезжает в
|
||||
`REJECTED.md`, а не заводится задачей.
|
||||
3. **Назови цель.** Выбранная форма служит цели — существующей или новой. Идея,
|
||||
для которой цель не находится, скорее всего уезжает в `REJECTED.md`, а не
|
||||
заводится задачей.
|
||||
4. **Только выбранную форму** дроби по тесту декомпозиции выше и проставь
|
||||
критерии приёмки: без них наследники останутся идеями под другим именем.
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# 🧹 `chore` — обслуживание, наблюдаемое поведение не меняется
|
||||
|
||||
Зависимости, сборка, перенос, чистка, оснастка. Отвечает на **«что нужно
|
||||
сделать»**, глаголом в неопределённой форме.
|
||||
|
||||
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
|
||||
Здесь только то, что у этого типа своё.
|
||||
|
||||
## Схема
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Заголовок отвечает на | что нужно сделать |
|
||||
| Обязательные разделы | `Затрагивает`, `Критерии приёмки` |
|
||||
| Допустимые сверх того | `Рамки`, `Вопросы` |
|
||||
| Поле места | **Категория** — полка домена беклога |
|
||||
| Цель (`goal:<слаг>`) | нет: цель — это возможность, а здесь её не появляется |
|
||||
| Индекс | `BACKLOG.md` → `SPRINT.md` |
|
||||
| Берётся в спринт | да |
|
||||
|
||||
## Адресат — разработчик, и это законно
|
||||
|
||||
Тест готовности спрашивает «что станет наблюдаемо иначе». У `chore` ответ
|
||||
адресован **разработчику**, а не пользователю: «перестанет собираться два раза»,
|
||||
«уедет последний вызов устаревшего API», «проверки гоняются одной командой».
|
||||
Это ответ, а не отговорка.
|
||||
|
||||
**У `chore` тест готовности слабее честно, а не молча.** Пока типа не было,
|
||||
такие задачи либо не заводились вовсе, либо формулировались как выдуманная
|
||||
пользовательская польза — и то и другое хуже, чем сказать прямо, для кого работа.
|
||||
|
||||
Отсюда же граница: если после задачи меняется то, что видит пользователь, — это
|
||||
не `chore`. Тип, оставшийся от первой формулировки, врёт ровно там, где по нему
|
||||
отбирают.
|
||||
|
||||
## Алгоритм
|
||||
|
||||
1. **Проверить, что поведение не меняется.** Меняется — это `feature` или `fix`,
|
||||
и у неё другие требования (цель, воспроизведение).
|
||||
2. **Назвать, что перестанет мешать** — одной фразой, адресуясь разработчику.
|
||||
«Прибраться в модуле X» — не ответ: непонятно, что изменится.
|
||||
3. **Назвать границы** в `Затрагивает`. У обслуживания они часто не в коде:
|
||||
конфиг и его образцы, версия зависимости, команда сборки, файл CI. Границей
|
||||
считается то, у чего есть внешняя сторона и цена изменения.
|
||||
4. **Написать критерии приёмки** — 2–5 утверждений с оракулами. У `chore`
|
||||
оракул обычно самый дешёвый из всех типов: команда, которая раньше падала
|
||||
или требовала трёх шагов, теперь отрабатывает одним.
|
||||
5. **Проверить, что это не «заодно».** Обслуживание любит склеиваться в пачку
|
||||
(«обновить зависимости и переписать сборку и убрать мёртвый код»). Не
|
||||
мерджится порознь — это несколько задач ([split.md](split.md)).
|
||||
6. **Цель не проставлять.** `chore` служит работоспособности, а не направлению,
|
||||
и в набор спринта входит помимо его цели. Работа по сопровождению проекта
|
||||
при этом видна в роадмапе — секцией `Сопровождение`, но целью не становится.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` и `sprint take` смотрят на **наличие непустого** `Затрагивает` и на
|
||||
**число** критериев — ровно то же, что у `feature`. Разница между типами здесь не
|
||||
в строгости проверки, а в том, **кому адресован ответ** на «что станет
|
||||
наблюдаемо иначе», — и это судит человек.
|
||||
@@ -0,0 +1,63 @@
|
||||
# ✨ `feature` — снаружи появляется то, чего не было
|
||||
|
||||
Задача, после которой наблюдаемое поведение меняется в сторону новой
|
||||
возможности. Отвечает на **«что нужно сделать»** и пишется глаголом в
|
||||
неопределённой форме.
|
||||
|
||||
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
|
||||
Здесь только то, что у этого типа своё.
|
||||
|
||||
## Схема
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Заголовок отвечает на | что нужно сделать («Печатать поле одним куском кода») |
|
||||
| Обязательные разделы | `Затрагивает`, `Критерии приёмки` |
|
||||
| Допустимые сверх того | `Рамки`, `Вопросы` |
|
||||
| Поле места | **Категория** — полка домена беклога |
|
||||
| Цель (`goal:<слаг>`) | **обязательна** |
|
||||
| Индекс | `BACKLOG.md` → `SPRINT.md` |
|
||||
| Берётся в спринт | да |
|
||||
|
||||
**Цель обязательна, и это единственный тип, у которого так.** Новая возможность
|
||||
и есть содержание цели: подходящей нет — либо она заводится, либо перед тобой не
|
||||
`feature`. `sprint take` без цели откажет.
|
||||
|
||||
## Алгоритм
|
||||
|
||||
1. **Найти цель или завести её.** Задача без цели, названная функцией, — самый
|
||||
частый способ пронести в беклог работу, которой никто не заказывал.
|
||||
2. **Назвать границы** в разделе `Затрагивает`: эндпоинт или команда, таблица и
|
||||
миграция, формат на диске, публичный тип пакета, внешний сервис. Названы
|
||||
**границы, а не замысел**: «переписать хранилище на новый драйвер» — замысел,
|
||||
`таблица points и её миграция` — граница. Проверяется вопросом «это можно
|
||||
назвать до того, как решено *как* делать?».
|
||||
3. **Написать критерии приёмки** — 2–5 проверяемых утверждений списком, у
|
||||
каждого назван оракул. Не «работает корректно», а «повторный прогон даёт тот
|
||||
же отпечаток — оракул: команда сверки».
|
||||
4. **Сказать, какую строку «Завершения» цели задача двигает.** Одной строкой в
|
||||
теле. Это защита от задачи «отрефакторить X»: она проваливается не потому,
|
||||
что невидима снаружи, а потому, что не находит строки, к которой относится.
|
||||
5. **Проверить, что задача одна.** Отвечается всё, но задача не делается одним
|
||||
заходом и не мерджится целиком — это несколько задач под одной целью, дроби
|
||||
сразу ([split.md](split.md)). Промежуточного зонтика между целью и задачей
|
||||
нет.
|
||||
6. **Реализация** — дело пайплайна проекта, не этого скилла. Закрывается
|
||||
`close <слаг> --implemented`: файл и строка удаляются, суть переезжает в
|
||||
`openspec/specs/` и документацию.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` и `sprint take` смотрят на **наличие непустого** раздела `Затрагивает`,
|
||||
на **число** критериев (меньше двух — отказ, больше пяти — замечание) и на цель.
|
||||
Наличие оракула проверяется **эвристикой** — словом «оракул» в пункте.
|
||||
|
||||
Полнота перечня границ машине не видна: границу, которую забыли назвать, она от
|
||||
отсутствующей не отличает. Настоящий оракул от слова «оракул» тоже не отличает.
|
||||
Поэтому в докладе это называется как есть: «проверено число пунктов и наличие
|
||||
границ, годность оракулов и полнота границ — глазами».
|
||||
|
||||
**Критерии — пол, но расхождение с ними есть дефект критериев.** Видишь, что
|
||||
критерии закрыты, а суть задачи не достигнута — **правь критерии и возвращай
|
||||
задачу**, а не держи невидимое сверх-требование: иначе исполнитель никогда не
|
||||
знает, закончил ли, и мотивирован занижать критерии заранее.
|
||||
@@ -0,0 +1,70 @@
|
||||
# 🐞 `fix` — поведение расходится с заявленным
|
||||
|
||||
Задача о расхождении между тем, что система делает, и тем, что про неё заявлено
|
||||
— в спеке, в инварианте `CLAUDE.md`, в критериях закрытой задачи. Отвечает на
|
||||
**«что нужно сделать»**, глаголом в неопределённой форме, перед ним допускается
|
||||
«не»: «Не отбрасывать молча лишние символы в ходе».
|
||||
|
||||
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
|
||||
Здесь только то, что у этого типа своё.
|
||||
|
||||
## Схема
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Заголовок отвечает на | что нужно сделать |
|
||||
| Обязательные разделы | **`Воспроизведение`**, `Затрагивает`, `Критерии приёмки` |
|
||||
| Допустимые сверх того | `Рамки`, `Вопросы` |
|
||||
| Поле места | **Категория** — полка домена беклога |
|
||||
| Цель (`goal:<слаг>`) | необязательна |
|
||||
| Индекс | `BACKLOG.md` → `SPRINT.md` |
|
||||
| Берётся в спринт | да |
|
||||
|
||||
## `Воспроизведение` — раздел, которого нет у других типов
|
||||
|
||||
**Не воспроизводится — это `research`, а не `fix`.** Правило было записано и
|
||||
раньше, но проверять его было нечем, и «починки» без единого шага повторения
|
||||
уходили в спринт наравне с остальными. Раздел делает правило проверяемым: он
|
||||
называет, **что сделать, чтобы расхождение проявилось, и что при этом видно
|
||||
вместо ожидаемого**.
|
||||
|
||||
Пишется двумя частями, обе обязательны по смыслу:
|
||||
|
||||
- **шаги или вход** — команда, запрос, файл, последовательность действий;
|
||||
- **что видно и что ожидалось** — «ввод `а1б2` ходит в `a1`, а должен быть
|
||||
отвергнут с ошибкой».
|
||||
|
||||
Это не критерии приёмки и не дублирует их: воспроизведение описывает **сегодня**,
|
||||
критерии — **завтра**. Пропущенное воспроизведение чаще всего означает одно из
|
||||
двух: расхождение приняли на слово, или его вообще нет, а есть недовольство
|
||||
поведением — и тогда это `feature`, а не `fix`.
|
||||
|
||||
## Алгоритм
|
||||
|
||||
1. **Воспроизвести.** Не удаётся — это `research`: заведи вопрос «при каких
|
||||
условиях проявляется» и не притворяйся, что чинить есть что.
|
||||
2. **Найти, чему поведение противоречит.** Спека, инвариант, критерий закрытой
|
||||
задачи. Не противоречит ничему — это `feature`: поведение никогда и не было
|
||||
заявлено, а тип, оставшийся от первой формулировки, врёт ровно там, где по
|
||||
нему отбирают.
|
||||
3. **Записать воспроизведение** — шаги и наблюдаемое против ожидаемого.
|
||||
4. **Назвать границы** в `Затрагивает`: починка часто трогает больше, чем
|
||||
кажется по объёму текста, и оценка систематически занижена именно здесь.
|
||||
5. **Написать критерии приёмки** — 2–5 утверждений с оракулами. У починки
|
||||
почти всегда есть парный критерий: **прежнее поведение не сломалось**
|
||||
(«ввод `а1` принимается по-прежнему»). Без него починка чинит одно и ломает
|
||||
соседнее.
|
||||
6. **Цель не выдумывать.** `fix` служит работоспособности, а не направлению, и в
|
||||
набор спринта входит помимо его цели. Придуманная цель — то же враньё, от
|
||||
которого спасает тип.
|
||||
7. **Записать дефект в журнал** `docs/review.md` с пометкой «проскочил / пойман
|
||||
ревью». Проскочившие — проверочный набор для калибровки конвейера; пойманные с
|
||||
оракулом — лучшая опора для прохода ревью: проектные, воспроизводимые,
|
||||
однажды оказавшиеся правдой.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` и `sprint take` смотрят на **наличие непустого** `Воспроизведения` и
|
||||
`Затрагивает` и на **число** критериев. Годность воспроизведения — человеку:
|
||||
шаги, по которым ничего не воспроизводится, машина от годных не отличает, и
|
||||
делать вид, что проверено больше проверенного, хуже, чем не проверять вовсе.
|
||||
@@ -1,78 +1,125 @@
|
||||
# Формат задач, целей и индексов
|
||||
# Формат записей и индексов
|
||||
|
||||
Заголовок, мета-блок и строку индекса ставит `tasks.py add` — руками их не
|
||||
пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`;
|
||||
тело задачи (одну фразу, критерии, рамки, контекст) дописывает агент.
|
||||
пишут. Этот файл описывает **общую форму** любой записи и то, что проверяет
|
||||
`check`; тело дописывает агент.
|
||||
|
||||
## Файл задачи
|
||||
Чем разделы тела отличаются от типа к типу, какой алгоритм у каждого типа и что
|
||||
у него обязательно — **отдельным файлом на тип**:
|
||||
|
||||
| Тип | Файл | Одной строкой |
|
||||
| --- | --- | --- |
|
||||
| 🎯 `goal` | [task-goal.md](task-goal.md) | возможность приложения |
|
||||
| ✨ `feature` | [task-feature.md](task-feature.md) | снаружи появляется то, чего не было |
|
||||
| 🐞 `fix` | [task-fix.md](task-fix.md) | поведение расходится с заявленным |
|
||||
| 🧹 `chore` | [task-chore.md](task-chore.md) | обслуживание, поведение не меняется |
|
||||
| 🔬 `research` | [task-research.md](task-research.md) | исход — знание, а не изменение |
|
||||
|
||||
## Файл записи
|
||||
|
||||
`items/<slug>.md`:
|
||||
|
||||
```markdown
|
||||
# Тай-брейк при равной полноте
|
||||
# 🐞 Не отбрасывать молча лишние символы в ходе
|
||||
|
||||
- **Секция:** Ядро — вышла из спринта: остаток писал нерешённое в журнал
|
||||
- **Зачем:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт
|
||||
- **Теги:** goal:merge-robustness, kind:fix, sprint:2026-08-03
|
||||
- **Тип:** fix
|
||||
- **Категория:** Ядро — вышла из спринта: остаток писал нерешённое в журнал
|
||||
- **Зачем:** ввод «а1б2» ходит в a1 — игрок не видит, что ошибся, и винит игру
|
||||
- **Теги:** goal:merge-robustness, sprint:2026-08-03
|
||||
|
||||
При столкновении точек выигрывает более полная, но при равной полноте побеждает
|
||||
последняя доставка — а она систематически беднее первой.
|
||||
Разбор хода читает первые два символа и молча выбрасывает остаток строки.
|
||||
|
||||
## Воспроизведение
|
||||
|
||||
Ввести `а1б2` в свой ход: программа ходит в `a1` и ничего не сообщает.
|
||||
Ожидалось — отказ с ошибкой разбора.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
Таблица `points` и её миграция; правило слияния в приёме доставки; формат
|
||||
отпечатка состояния на диске. Публичного контракта не трогает.
|
||||
Разбор строки хода; текст ошибки в выводе партии. Формат сохранения партии
|
||||
не трогается.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- повторный прогон свёртки даёт тот же отпечаток состояния — оракул: команда сверки
|
||||
- накопительная метрика за сутки не уменьшается после повторной доставки — оракул: тест
|
||||
- в логе видно, какая из двух точек выиграла и почему — оракул: глазами по логу прогона
|
||||
- ввод «а1б2» отвергается с ошибкой — оракул: тест разбора
|
||||
- ввод «а1» принимается по-прежнему — оракул: тест разбора
|
||||
|
||||
## Рамки
|
||||
|
||||
Схема не трогается; данные только читаются; перезапуск сервиса допустим.
|
||||
Схема не трогается; данные только читаются; перезапуск допустим.
|
||||
|
||||
Связано: решение о канонической форме содержимого.
|
||||
```
|
||||
|
||||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
|
||||
префиксом `[goal]` / `[idea]`; обычная задача — без префикса.
|
||||
Отдельного поля типа **нет**: два места для одного факта разъезжаются, а
|
||||
префикс виден прямо в индексе, где и принимается решение «брать или не брать».
|
||||
- **Форма заголовка — по типу записи.** Задача отвечает на «что нужно сделать»
|
||||
и пишется глаголом в неопределённой форме («Печатать поле одним куском кода»,
|
||||
«Не отбрасывать молча лишние символы»); цель — на «что приложение будет
|
||||
уметь»; идея просто называет, о чём она. Почему так — SKILL.md, «Как написана
|
||||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Начинается
|
||||
**эмодзи типа**, и она **производна**: её ставит `add` и чинит `check --fix`
|
||||
по полю меты. Второго дома у типа нет — эмодзи это его отображение, как
|
||||
строка индекса это отображение файла.
|
||||
- **Форма заголовка — по типу.** Цель отвечает на «что приложение будет уметь»;
|
||||
`feature`, `fix` и `chore` — на «что нужно сделать», глаголом в неопределённой
|
||||
форме, перед ним допускается «не»; `research` называет предмет разведки и
|
||||
формы действия **не несёт намеренно**. Почему так — SKILL.md, «Как написана
|
||||
задача». `check` считает заголовки не в форме действия и печатает число в
|
||||
здоровье; годность формулировки смотрит агент `task-form`.
|
||||
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательна
|
||||
секция, причина после тире желательна (именно она объясняет, почему задача
|
||||
здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и теги
|
||||
опциональны. Порядок свободный, поле в одну строку. Нераспознанные поля
|
||||
сохраняются: скрипт правит свои и не трогает чужие.
|
||||
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательны
|
||||
**тип** и **место**, причина после тире желательна (именно она объясняет,
|
||||
почему задача здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и
|
||||
теги необязательны. Нераспознанные поля сохраняются: скрипт правит свои и не
|
||||
трогает чужие.
|
||||
- **Тип — первым полем.** Он решает, что у записи вообще может быть: какие
|
||||
разделы обязательны, нужна ли цель, берётся ли она в спринт, — и читается
|
||||
раньше всего остального. Словарь **закрыт**: `goal` | `feature` | `fix` |
|
||||
`chore` | `research`. Не подходит ни один — это сигнал, что в записи их два и
|
||||
её надо разделить.
|
||||
- **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль.
|
||||
Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в
|
||||
индексе: строка индекса его повторяет и производна от него, `check` сверяет,
|
||||
`check --fix` восстанавливает пропавшую строку **вместе с ним**. Пока поле
|
||||
лежало только в индексе, штатная починка дрейфа теряла его молча и
|
||||
навсегда — а это единственное, по чему задачу выбирают, не открывая.
|
||||
|
||||
- **Тело** — одна фраза «что станет наблюдаемо иначе», затрагиваемые границы,
|
||||
критерии приёмки, рамки, контекст, ссылки. Пишется на языке документации
|
||||
проекта: предметно, без англицизмов, у которых есть русское слово, и без
|
||||
терминов, которых нет ни в паспорте, ни в архитектуре, ни в конвенциях
|
||||
(правило и его причина — в SKILL.md, раздел «Как написана задача»).
|
||||
|
||||
Мета **одной строкой через `·`** — прежняя форма. Она читается по-прежнему,
|
||||
`check` называет её дрейфом, `check --fix` переписывает списком; поле `Хук`
|
||||
при этом становится `Зачем`. Причина отказа от строки простая: с тремя полями
|
||||
и длинным «зачем» строка уезжала за экран, а `·` приходилось запрещать в тексте
|
||||
причины и самого «зачем».
|
||||
- **Тело** — одна фраза «что станет наблюдаемо иначе», дальше разделы по схеме
|
||||
типа. Пишется на языке документации проекта: предметно, без англицизмов, у
|
||||
которых есть русское слово, и без терминов, которых нет ни в паспорте, ни в
|
||||
архитектуре, ни в конвенциях (правило и его причина — в SKILL.md, раздел «Как
|
||||
написана задача»).
|
||||
|
||||
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
|
||||
в документацию проекта, а файл задачи удаляется.
|
||||
|
||||
### Поле места: «Категория» и «Секция»
|
||||
|
||||
Поле называет, **где числится строка**, и имя у него **зависит от типа**:
|
||||
|
||||
| Тип | Поле | Значения | Что это |
|
||||
| --- | --- | --- | --- |
|
||||
| `goal` | **Секция** | `Запланировано`, `Направления`, `Сопровождение` | часть роадмапа: состояние очереди |
|
||||
| прочие | **Категория** | секции беклога проекта (`Ядро`, `Инфра`, …) | полка домена, в которую задача вернётся из спринта |
|
||||
|
||||
Разные имена потому, что это **разные вещи**. У задачи поле переживает спринт:
|
||||
`sprint drop` возвращает её именно туда. У цели оно называет не полку, а место в
|
||||
очереди работ. Одно имя на два смысла их и смешивало; `check` называет
|
||||
несовпадение дрейфом, `check --fix` переименовывает.
|
||||
|
||||
Имя самого места принадлежит **заголовку индекса** — файл на него лишь
|
||||
ссылается, и принадлежность сверяется по нижнему регистру.
|
||||
|
||||
### Прежние формы, которые читаются, но не пишутся
|
||||
|
||||
Всё это `check` называет дрейфом, а `check --fix` переписывает:
|
||||
|
||||
| Было | Стало |
|
||||
| --- | --- |
|
||||
| префикс `[goal]` / `[idea]` в H1 | поле **Тип** + эмодзи в H1; `[idea]` → `research` |
|
||||
| тег `kind:<род>` | поле **Тип** (род работы стал типом) |
|
||||
| поле **Секция** у задачи | поле **Категория** |
|
||||
| поле **Хук** | поле **Зачем** |
|
||||
| мета одной строкой через `·` | мета списком, поле на строку |
|
||||
|
||||
Единственное, чего `--fix` не делает сам, — **проставить тип записи, у которой
|
||||
его неоткуда взять**: `feature` от `chore` машина не отличает, и подставленное
|
||||
наугад значение врало бы ровно там, где по нему принимают решение. Такие записи
|
||||
он называет поимённо пометкой `НЕОДНОЗНАЧНО`.
|
||||
|
||||
### Затрагивает
|
||||
|
||||
Перечень **границ**, которых изменение касается. Границей считается то, у чего
|
||||
@@ -100,8 +147,8 @@
|
||||
забыли назвать, она от отсутствующей не отличает. Раздела нет — отказ во взятии:
|
||||
оценивать нечем.
|
||||
|
||||
**У идей раздела нет** — как и критериев: границы становятся известны, когда идея
|
||||
превращается в задачу.
|
||||
**У `goal` и `research` раздела нет** — у первой границы называют её задачи, у
|
||||
второй они становятся известны, когда из разведки родятся задачи.
|
||||
|
||||
### Критерии приёмки
|
||||
|
||||
@@ -118,7 +165,9 @@
|
||||
замечание: настоящий оракул от слова «оракул» машина не отличает, и делать вид,
|
||||
что проверено больше проверенного, хуже, чем не проверять вовсе.
|
||||
|
||||
**У идей критериев нет — именно поэтому они идеи.**
|
||||
**У `research` критериев нет** — её приёмка это записанный ответ, и описывается
|
||||
она разделами «Вопрос» и «Куда ляжет ответ». **У `goal` их заменяет
|
||||
«Завершение».**
|
||||
|
||||
**Критерии — пол, но расхождение с ними есть дефект критериев.** Если приёмщик
|
||||
видит, что критерии закрыты, а суть задачи не достигнута, он **правит критерии и
|
||||
@@ -129,16 +178,21 @@
|
||||
### Рамки
|
||||
|
||||
Одна строка: чего касаться нельзя, что перезапускается, что считается
|
||||
необратимым, трогается ли схема данных. **Свойства репозитория сюда не пишутся**
|
||||
— номер последней миграции, версия зависимости, хеш: в лежалой задаче они
|
||||
протухают молча и становятся ложной рамкой. Снимок берётся при постановке, а не
|
||||
при заведении.
|
||||
необратимым, трогается ли схема данных. Раздел **допустим у любого типа задачи и
|
||||
ни у одного не обязателен**. **Свойства репозитория сюда не пишутся** — номер
|
||||
последней миграции, версия зависимости, хеш: в лежалой задаче они протухают
|
||||
молча и становятся ложной рамкой. Снимок берётся при постановке, а не при
|
||||
заведении.
|
||||
|
||||
### Вопросы
|
||||
|
||||
Неразобранное решение человека живёт разделом `## Вопросы` **плюс тегом
|
||||
`question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет.
|
||||
|
||||
Раздел `Вопросы` (о решении человека) и раздел `Вопрос` у `research` (предмет
|
||||
разведки) — **разные вещи и разные слова**: первый блокирует взятие, второй его
|
||||
разрешает.
|
||||
|
||||
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
|
||||
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
|
||||
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
|
||||
@@ -158,13 +212,12 @@
|
||||
|
||||
## Файл цели
|
||||
|
||||
**Заголовок цели отвечает на «что приложение будет уметь».** Не область работ и
|
||||
не имя подсистемы: не «Работа со слиянием», а «Исход слияния не зависит от
|
||||
порядка доставки». Свойство поведения — тоже возможность.
|
||||
Форма та же, разделы и алгоритм — [task-goal.md](task-goal.md).
|
||||
|
||||
```markdown
|
||||
# [goal] Исход слияния не зависит от порядка доставки
|
||||
# 🎯 Исход слияния не зависит от порядка доставки
|
||||
|
||||
- **Тип:** goal
|
||||
- **Секция:** Направления
|
||||
- **Теги:** decomposed
|
||||
|
||||
@@ -181,10 +234,6 @@
|
||||
- **Задачи цели здесь не перечисляются.** Перечень даёт
|
||||
`tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и
|
||||
поехал бы на первой же закрытой задаче.
|
||||
- **Раздел «Завершение» — списком, а не абзацем.** Это признаки того, что
|
||||
приложение уже умеет; **на строку «Завершения» ссылается задача**, объясняя,
|
||||
какую часть возможности она двигает (см. тест готовности). Абзацем такая
|
||||
ссылка не берётся, поэтому список.
|
||||
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
|
||||
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
|
||||
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
|
||||
@@ -217,16 +266,18 @@
|
||||
Строка везде одной формы:
|
||||
|
||||
```markdown
|
||||
- [Заголовок дословно](items/slug.md) — зачем
|
||||
- [🐞 Заголовок дословно](items/slug.md) — зачем
|
||||
```
|
||||
|
||||
«Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние,
|
||||
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
|
||||
Эмодзи внутри квадратных скобок не украшение: заголовок копируется **дословно**,
|
||||
и тип виден там, где решают «брать или не брать».
|
||||
|
||||
| Файл | Что отвечает | Секции |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические: `Готово`, `Запланировано`, `Направления`, `Разработка` (англ. `Done`, `Planned`, `Directions`, `Tooling`) |
|
||||
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию Ядро/Инфра) |
|
||||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические и в этом порядке: `Запланировано`, `Направления`, `Сопровождение`, `Готово` (англ. `Planned`, `Directions`, `Operations`, `Done`) |
|
||||
| `BACKLOG.md` | что **можно взять** — только задачи | категории проекта (по умолчанию Ядро/Инфра) |
|
||||
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
||||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||||
|
||||
@@ -237,8 +288,15 @@
|
||||
следующем `sprint start` и очищается на `sprint close`.
|
||||
|
||||
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
|
||||
преамбуле проверка сочтёт секцией. Внутри секции беклога порядок значения не
|
||||
имеет — порядка в беклоге нет вовсе.
|
||||
преамбуле проверка сочтёт секцией.
|
||||
|
||||
**Порядка «по важности» внутри секции беклога нет** — «что делать дальше»
|
||||
отвечает набор спринта. Единственный порядок, который есть, **производен от типа
|
||||
и заполненности**: **сырьё** (`research` без раздела «Вопрос») стоит в конце
|
||||
своей секции, потому что его не берут, и между берущимся оно каждый раз требует
|
||||
открыть файл, чтобы это понять. Проверяет `check`, переставляет `check --fix`,
|
||||
и человек этот порядок не назначает — иначе он был бы приоритетом, которого
|
||||
здесь нет.
|
||||
|
||||
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
|
||||
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
|
||||
@@ -250,14 +308,15 @@
|
||||
строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в
|
||||
`REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда).
|
||||
|
||||
**Секции роадмапа закреплены** — состав, полнота и единство языка проверяются
|
||||
`check`; секции беклога проект называет сам. Почему так — SKILL.md.
|
||||
**Секции роадмапа закреплены** — состав, полнота, единство языка и **порядок**
|
||||
проверяются `check`; категории беклога проект называет сам. Почему так —
|
||||
SKILL.md. Порядок закреплён потому, что `Готово` копится: стоя первым,
|
||||
достигнутое отодвигает за экран то, ради чего роадмап открывают чаще всего.
|
||||
|
||||
**Заголовок секции пишется с прописной, и после него идёт пустая строка** — во
|
||||
всех индексах, включая секции беклога, имена которых выбирает проект. Написание
|
||||
канонических секций и отбивку правит `check --fix`; он же сводит написание
|
||||
секции в мете файла с заголовком индекса — **имя секции принадлежит заголовку**,
|
||||
файл на неё лишь ссылается, и принадлежность сверяется по нижнему регистру.
|
||||
**Заголовок секции пишется с прописной и отбивается пустой строкой с обеих
|
||||
сторон** — во всех индексах, включая категории беклога, имена которых выбирает
|
||||
проект. Написание канонических секций и отбивку правит `check --fix`; он же
|
||||
сводит написание места в мете файла с заголовком индекса.
|
||||
|
||||
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
|
||||
Строку руками не пишут.
|
||||
@@ -293,19 +352,13 @@
|
||||
|
||||
## Теги
|
||||
|
||||
Единственный механизм разметки, потому что `list --tag` уже умеет отбирать по
|
||||
ним порцию разбора. Отдельных полей меты под это не заводим.
|
||||
Разметка сверх типа. Тип полем, потому что он один и обязателен; теги — потому
|
||||
что их много и `list --tag` уже умеет отбирать по ним порцию разбора.
|
||||
|
||||
- `goal:<слаг>` — цель, которой служит задача. Обязателен **у `kind:feature`**:
|
||||
- `goal:<слаг>` — цель, которой служит задача. Обязателен **у `feature`**:
|
||||
новая возможность и есть содержание цели. У `fix`, `chore` и `research` его
|
||||
может не быть — они служат работоспособности, а не направлению, и в набор
|
||||
спринта входят помимо его цели.
|
||||
- `kind:<род>` — род работы: `feature` | `fix` | `chore` | `research`. Словарь
|
||||
**закрыт**, значение ровно одно. Обязателен у задачи (без него `sprint take`
|
||||
откажет), у цели запрещён, у идеи необязателен. Ставится
|
||||
`add --kind` / `edit --kind`; `--kind` заменяет прежнее значение, а не
|
||||
добавляет второе. Смысл рода и почему он тегом, а не префиксом — в SKILL.md,
|
||||
раздел «Род работы».
|
||||
- `question` — в файле есть неразобранный раздел «Вопросы».
|
||||
- `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая
|
||||
порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит
|
||||
@@ -315,6 +368,9 @@
|
||||
разбора — урожай прошедшего спринта».
|
||||
- `decomposed` — на цели: разложена на задачи (см. «Файл цели»).
|
||||
|
||||
Тега `kind:<род>` больше нет: род работы стал типом. Оставшийся в файле `check`
|
||||
называет дрейфом, а `check --fix` снимает, перенеся значение в поле «Тип».
|
||||
|
||||
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
|
||||
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
|
||||
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
|
||||
@@ -325,17 +381,20 @@
|
||||
|
||||
## Тест «готова к взятию»
|
||||
|
||||
Задача готова, если из файла отвечаются четыре вопроса:
|
||||
Задача готова, если из файла отвечаются четыре вопроса. Первый и четвёртый —
|
||||
общие, второй и третий у каждого типа свои и перечислены в его файле.
|
||||
|
||||
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
|
||||
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
|
||||
ломаться Y при Z» — ответ. **У `kind:chore` адресат — разработчик, и это
|
||||
ломаться Y при Z» — ответ. **У `chore` адресат — разработчик, и это
|
||||
законно**: «уедет последний вызов устаревшего API» — ответ, а не отговорка.
|
||||
Род объявлен как раз затем, чтобы такие задачи не выдумывали себе
|
||||
Тип объявлен как раз затем, чтобы такие задачи не выдумывали себе
|
||||
пользовательскую пользу.
|
||||
2. **Каких границ это касается** — раздел «Затрагивает». Без него задачу нельзя
|
||||
оценить: остаётся судить по длине текста.
|
||||
3. **По чему видно, что закончено** — критерии приёмки с оракулами.
|
||||
2. **Что известно про сегодня** — то, что тип требует знать до работы:
|
||||
у `fix` это `Воспроизведение`, у `research` — `Вопрос`, у `feature` и
|
||||
`chore` — `Затрагивает`.
|
||||
3. **По чему видно, что закончено** — критерии приёмки с оракулами;
|
||||
у `research` вместо них `Куда ляжет ответ`.
|
||||
4. **Какую часть «Завершения» своей цели она двигает** — у задачи с целью.
|
||||
Строкой: «двигает пункт 2 «Завершения» — накопительная метрика перестаёт
|
||||
уменьшаться». Это и есть защита от задачи «отрефакторить X»: она проваливает
|
||||
@@ -347,9 +406,10 @@
|
||||
**У задачи без цели** (`fix`, `chore`, `research`) вопрос не задаётся: они
|
||||
служат работоспособности, а не направлению.
|
||||
|
||||
Не отвечается первый, второй или третий вопрос → это **идея** (`[idea]`), её
|
||||
место в штурме. Не отвечается четвёртый у `feature` → либо цель есть и не
|
||||
проставлена, либо это не новая возможность.
|
||||
Не отвечается первый, второй или третий вопрос → это ещё не задача, а **сырьё**:
|
||||
тип `research` без раздела «Вопрос», место — конец секции, работа над ним —
|
||||
штурм. Не отвечается четвёртый у `feature` → либо цель есть и не проставлена,
|
||||
либо это не новая возможность.
|
||||
|
||||
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
|
||||
это **несколько задач под одной целью**, дроби сразу. Промежуточного зонтика
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# 🎯 `goal` — возможность приложения
|
||||
|
||||
Цель отвечает на **«что приложение будет уметь»**. Не область работ и не имя
|
||||
подсистемы: не «Работа со слиянием», а «Исход слияния не зависит от порядка
|
||||
доставки». Свойство поведения — тоже возможность.
|
||||
|
||||
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
|
||||
Здесь только то, что у этого типа своё.
|
||||
|
||||
## Схема
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Заголовок отвечает на | что приложение будет уметь |
|
||||
| Обязательные разделы | `Завершение` |
|
||||
| Допустимые сверх того | — |
|
||||
| Поле места | **Секция** — часть роадмапа |
|
||||
| Цель (`goal:<слаг>`) | запрещена: цель и есть цель |
|
||||
| Индекс | `ROADMAP.md`, и никогда `BACKLOG.md` или `SPRINT.md` |
|
||||
| Берётся в спринт | нет — берутся её задачи |
|
||||
|
||||
Поле места у цели называется **«Секция»**, а не «Категория», и это не разнобой:
|
||||
у задачи оно называет полку домена, в которую она вернётся из спринта, а у цели
|
||||
— часть роадмапа, то есть состояние очереди. Одно имя на два смысла их и
|
||||
смешивало.
|
||||
|
||||
## «Завершение» — списком, а не абзацем
|
||||
|
||||
Это признаки того, что приложение **уже умеет**, и на строки этого раздела
|
||||
ссылаются задачи цели: «двигает пункт 2 «Завершения» — накопительная метрика
|
||||
перестаёт уменьшаться». Абзацем такая ссылка не берётся, поэтому список.
|
||||
|
||||
Отсюда же читается обратное и более полезное: **строка «Завершения», к которой
|
||||
не относится ни одна задача, — незакрытая часть возможности**. Достаточность
|
||||
набора задач видна из самой цели, а не из чьей-то памяти.
|
||||
|
||||
## Алгоритм
|
||||
|
||||
1. **Проверить, что это возможность, а не работа.** Работа, которой держат
|
||||
проект, на вопрос «что приложение будет уметь» не отвечает; состав перечислен
|
||||
[в каноне](../../canon/references/canon.md), раздел «Сопровождение и
|
||||
эксплуатация». Ей отведена секция `Сопровождение` — там она видна в том же
|
||||
экране и не читается как обещание продукта. Граница проходит по тому,
|
||||
**кто наблюдает**:
|
||||
«приложение сообщает о своём состоянии» — возможность, «дежурный видит
|
||||
состояние на одном экране» — сопровождение.
|
||||
2. **Выбрать секцию.** Очередь значима и обоснована прозой — `Запланировано`;
|
||||
тянется долго и очереди не имеет — `Направления`; про то, чем держат проект,
|
||||
— `Сопровождение`. В `Готово` кладёт сам `close`.
|
||||
3. **Написать «Завершение»** — 2–5 наблюдаемых признаков списком. Пишутся до
|
||||
декомпозиции: иначе задачи придумают себе цель задним числом.
|
||||
4. **Разложить на задачи** и проставить им `goal:<слаг>`. Перечень задач в теле
|
||||
цели **не хранится** — он был бы третьим индексом и поехал бы на первой же
|
||||
закрытой задаче; выводит `tasks.py list --goal <слаг>`.
|
||||
5. **Пометить `decomposed`.** Тег отличает «ещё не разобрана» от «все задачи
|
||||
закрыты» — два состояния с одним внешним признаком. `check --fix` ставит его
|
||||
сам цели, у которой задачи есть.
|
||||
6. **Закрыть достигнутой** — `close <слаг> --implemented`, когда не осталось
|
||||
открытых задач. Файл удаляется, строка с датой переезжает в `Готово`. Скрипт
|
||||
откажет, если задачи ещё живы.
|
||||
|
||||
## Отменённая цель — сперва задачи, потом цель
|
||||
|
||||
Замысел бывает неверен, и цель отменяют, не достигнув. Порядок обратный
|
||||
завершению и держится тем же запретом: цель, закрытая поверх живых задач,
|
||||
оставила бы их сиротами, и `close` этого не даст.
|
||||
|
||||
1. **Разобрать её задачи поштучно.** Задача, теряющая смысл вместе с целью, —
|
||||
`close <slug> --reason "<почему>"`; задача, переживающая цель, — `edit <slug>
|
||||
--goal <другая>`. **Причина обязательна и пишется своя каждой:** «цель
|
||||
отменена» это не причина, а пересказ команды, и в `REJECTED.md` от него нет
|
||||
пользы через квартал.
|
||||
2. **Закрыть саму цель** — `close <слаг> --reason "<почему замысел отменён>"`.
|
||||
Файл удаляется, строка с причиной и датой уходит в `REJECTED.md`. В `Готово`
|
||||
не попадает: `Готово` отвечает «что приложение умеет», а отменённая цель не
|
||||
умеет ничего.
|
||||
|
||||
**Место этому — переоценка на сессии, а не отдельный заход.** Отмена цели значит
|
||||
разбор всех её задач, а разбор задач и есть шаг 3 сессии
|
||||
([cadence.md](../../session/references/cadence.md), пункт 7). Отменять на ходу,
|
||||
между делом, — верный способ закрыть скопом то, что стоило перевесить.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` считает цели, различает разобранные и пустые, ставит `decomposed`,
|
||||
запрещает закрыть цель с живыми задачами и держит `Секцию` в согласии с
|
||||
заголовком роадмапа. **Годность формулировки — не машине**: «возможность это или
|
||||
область работ» решает [агент вычитки](../SKILL.md#вычитка-два-прохода-а-не-один).
|
||||
|
||||
Достигнутая цель **не исчезает**: «что приложение умеет» — половина вопроса, ради
|
||||
которого роадмап открывают. Вторым домом поведения роадмап при этом не
|
||||
становится: нормативное поведение живёт в `openspec/specs/`, роадмап отвечает,
|
||||
**когда и в каком порядке** оно появилось.
|
||||
@@ -0,0 +1,83 @@
|
||||
# 🔬 `research` — исход работы знание, а не изменение системы
|
||||
|
||||
Ответ на вопрос, замер, разведка, проработка сырой мысли. Приёмка — **записанный
|
||||
ответ**, а не изменённый код.
|
||||
|
||||
Общая форма записи (мета, слаг, строка индекса) — [task-format.md](task-format.md).
|
||||
Здесь только то, что у этого типа своё.
|
||||
|
||||
## Схема
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Заголовок отвечает на | о чём разведка (предмет, а не действие) |
|
||||
| Обязательные разделы | `Вопрос`, `Куда ляжет ответ` |
|
||||
| Допустимые сверх того | `Рамки`, `Вопросы` |
|
||||
| Поле места | **Категория** — полка домена беклога |
|
||||
| Цель (`goal:<слаг>`) | нет |
|
||||
| Индекс | `BACKLOG.md` → `SPRINT.md` |
|
||||
| Берётся в спринт | да — **но только с заполненным «Вопросом»** |
|
||||
|
||||
**Критериев приёмки у `research` нет, и это не поблажка.** Критерии в форме
|
||||
«оракул: тест» разведке натянуты: проверять нечего, пока ответа нет. Её приёмка
|
||||
описывается раздельно — вопрос, на который отвечаем, и место, куда ляжет ответ.
|
||||
|
||||
**Заголовок формы действия не несёт намеренно.** Что делать, ещё неизвестно, и
|
||||
заголовок-действие обещал бы решённость, которой нет. «Подсказка следующего
|
||||
хода», а не «Сделать подсказку следующего хода».
|
||||
|
||||
## Этот тип вобрал прежний `[idea]`
|
||||
|
||||
Тип `idea` упразднён. Он значил не род работы, а **состояние незаполненности** —
|
||||
«первый, второй или третий вопрос теста готовности не отвечается», — а состояние
|
||||
типом быть не может: оно меняется по мере того, как запись дописывают, а тип
|
||||
меняют командой.
|
||||
|
||||
Теперь это состояние называется честно: **`research` без раздела «Вопрос» — это
|
||||
сырьё**.
|
||||
|
||||
| | сырьё | разведка |
|
||||
| --- | --- | --- |
|
||||
| Раздел `Вопрос` | пуст или отсутствует | заполнен |
|
||||
| `sprint take` | отказ | берёт |
|
||||
| Место в секции беклога | **конец**, `check --fix` сносит туда сам | среди прочих |
|
||||
| `tasks.py list --raw` | показывает | нет |
|
||||
|
||||
Порядка «по важности» в беклоге по-прежнему нет. Этот порядок **производен от
|
||||
типа и заполненности**, а не назначен человеком, — потому его и проверяет машина,
|
||||
и потому он не противоречит правилу «порядка нет, есть цель».
|
||||
|
||||
Сырьём заводится и **сырая функция**: «Подсказка следующего хода» — ещё не
|
||||
`feature`, потому что неизвестно, что именно делать. Работа над ней — думание, и
|
||||
её исход — либо задачи, либо отказ.
|
||||
|
||||
## Алгоритм
|
||||
|
||||
1. **Записать вопрос одной фразой.** Не тему, а вопрос: не «Разобраться с
|
||||
выводом в терминалах», а «Какими символами рамки печатаются одинаково в
|
||||
Терминале, iTerm и `tmux`». Вопроса ещё нет — запись заводится сырьём и
|
||||
лежит в конце секции, пока вопрос не появится.
|
||||
2. **Назвать, куда ляжет ответ**: `docs/research/<slug>.md`, ADR, тело этой
|
||||
задачи. Место называется **заранее**, иначе ответ остаётся в переписке, а
|
||||
через квартал разведку заказывают заново.
|
||||
3. **Ограничить рамками**, если разведка может утечь: сколько времени, какие
|
||||
источники, что заведомо вне.
|
||||
4. **Провести разведку** и **записать ответ по названному адресу**. Числа — с
|
||||
провенансом: с командой или условиями, которыми получены. Число без источника
|
||||
проход ревью обязан читать как условие, а не как замер.
|
||||
5. **Разложить исход на задачи** — если он их родил. Разведка кончается одним из
|
||||
трёх: заведены задачи, записано знание, отказ. **Отказ — полноправный исход**:
|
||||
«проверили, не проблема» экономит спринт.
|
||||
6. **Закрыть** — `close <слаг> --implemented`, когда ответ записан. Файл
|
||||
удаляется: запись ответа и есть след, второго не нужно. Ушла без ответа —
|
||||
`close --reason`, и строка уезжает в `REJECTED.md`.
|
||||
|
||||
## Что видит машина, а что человек
|
||||
|
||||
`check` и `sprint take` смотрят на **наличие непустых** разделов `Вопрос` и
|
||||
`Куда ляжет ответ`, считают сырьё отдельной строкой здоровья и держат его в конце
|
||||
секции. Годность вопроса — человеку: «вопрос это или тема» машина не различает,
|
||||
и `check` о годности молчит намеренно.
|
||||
|
||||
Штурм сырья, дробление исхода на задачи и тест «части мерджатся порознь» —
|
||||
[split.md](split.md).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,6 @@ package = false
|
||||
[tool.ruff]
|
||||
target-version = "py312"
|
||||
line-length = 88
|
||||
# backlog.py заморожен: плагин помечен УСТАРЕЛ и живёт до перевода последнего
|
||||
# проекта, после чего удаляется целиком. Правки в него — риск без выгоды.
|
||||
exclude = ["av-dev-backlog"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
|
||||
+6
-3
@@ -15,11 +15,14 @@
|
||||
|
||||
<!-- дом: <id> -->
|
||||
…текст…
|
||||
<!-- /дом -->
|
||||
<!-- /дом: <id> -->
|
||||
|
||||
<!-- копия: <id> из <путь к файлу дома> -->
|
||||
…тот же текст…
|
||||
<!-- /копия -->
|
||||
<!-- /копия: <id> -->
|
||||
|
||||
Закрывающий маркер несёт **тот же id**, что открывающий: без него не отличить
|
||||
конец своего блока от конца соседнего, а вложенных блоков разметка не знает.
|
||||
|
||||
Сверяется текст **между** маркерами: построчно, с отброшенными хвостовыми
|
||||
пробелами и пустыми строками по краям. Всё остальное вокруг копии — предисловие,
|
||||
@@ -43,7 +46,7 @@ from pathlib import Path
|
||||
|
||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||
|
||||
SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__", "av-dev-backlog"}
|
||||
SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__"}
|
||||
|
||||
# Идентификатор — только буквы, цифры и дефис. Строгость намеренная: она же
|
||||
# отличает **настоящий** маркер от примера в документации об этом механизме.
|
||||
|
||||
Reference in New Issue
Block a user