Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbfae90f3f
|
||
|
|
47a2f3de63
|
||
|
|
2d39a77444
|
||
|
|
c3e0a6d01f
|
||
|
|
52cc4d05d4
|
||
|
|
354a6b03d5
|
||
|
|
228b6c7eee
|
||
|
|
069205ac69
|
||
|
|
d7e9740c73
|
@@ -19,11 +19,6 @@
|
|||||||
"name": "av-dev-git",
|
"name": "av-dev-git",
|
||||||
"source": "./av-dev-git",
|
"source": "./av-dev-git",
|
||||||
"description": "Git-обвязка для личных проектов. Скилл commit — сообщения коммитов в личном стиле (русский, scope-префикс, средняя детализация, без co-authored)."
|
"description": "Git-обвязка для личных проектов. Скилл commit — сообщения коммитов в личном стиле (русский, scope-префикс, средняя детализация, без co-authored)."
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "av-dev-backlog",
|
|
||||||
"source": "./av-dev-backlog",
|
|
||||||
"description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями. Оставлен до перевода последнего проекта; новые проекты не подключают."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,6 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
.venv/
|
.venv/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
/NOTES.md
|
||||||
|
|||||||
+537
-7
@@ -261,7 +261,7 @@ jellybit — секреты в `conventions/config.md`. **Периметра н
|
|||||||
проверять сознательно` требует ссылки на его запись.
|
проверять сознательно` требует ссылки на его запись.
|
||||||
|
|
||||||
**L. Журнал расширяется до всех воспроизведённых дефектов** с пометкой
|
**L. Журнал расширяется до всех воспроизведённых дефектов** с пометкой
|
||||||
«проскочил / пойман ревью». Эвал-сет для калибровки — выборка по пометке.
|
«проскочил / пойман ревью». Проверочный набор для калибровки — выборка по пометке.
|
||||||
|
|
||||||
*Причина:* пойманные дефекты с оракулом (768 МиБ пика, 5.019 с удержания
|
*Причина:* пойманные дефекты с оракулом (768 МиБ пика, 5.019 с удержания
|
||||||
блокировки) сегодня не сохраняются нигде, кроме отчётов триажа в архиве change, а
|
блокировки) сегодня не сохраняются нигде, кроме отчётов триажа в архиве change, а
|
||||||
@@ -373,8 +373,9 @@ openspec/
|
|||||||
докладывает исход, записей учёта не трогает.
|
докладывает исход, записей учёта не трогает.
|
||||||
|
|
||||||
**Q. `av-dev-backlog` помечается устаревшим и остаётся** до перевода jellybit.
|
**Q. `av-dev-backlog` помечается устаревшим и остаётся** до перевода jellybit.
|
||||||
Описание переписывается так, чтобы не ловить триггер «добавь задачу в беклог» —
|
*(заменено на тему 30: плагин удалён раньше этого срока — условие пережило свою
|
||||||
иначе агент выбирает между ним и `av-dev-pm` случайно.
|
причину.)* Описание переписывается так, чтобы не ловить триггер «добавь задачу в
|
||||||
|
беклог» — иначе агент выбирает между ним и `av-dev-pm` случайно.
|
||||||
|
|
||||||
### Что из этого следует
|
### Что из этого следует
|
||||||
|
|
||||||
@@ -506,8 +507,8 @@ check`). Плюс `openspec/specs/` вливает `opsx:archive`.
|
|||||||
|
|
||||||
*Причина:* это тот же приём, что «границы покрытия» в отчёте ревью и «пустой
|
*Причина:* это тот же приём, что «границы покрытия» в отчёте ревью и «пустой
|
||||||
пункт называется пустым» в каноне — и единственный, о котором в этом репозитории
|
пункт называется пустым» в каноне — и единственный, о котором в этом репозитории
|
||||||
есть данные, что он работает. Умолчание «не написал» становится неотличимым от
|
есть данные, что он работает. Отличить «не написал» от «написал, что не
|
||||||
«написал, что не требуется», только если отрицание обязательно.
|
требуется» можно только тогда, когда отрицание обязательно.
|
||||||
|
|
||||||
**Триггер ADR, окончательная формулировка.** Запись заводится, когда верно одно
|
**Триггер ADR, окончательная формулировка.** Запись заводится, когда верно одно
|
||||||
из трёх: **дорогой откат** (переделка стоит дороже переписывания одного файла);
|
из трёх: **дорогой откат** (переделка стоит дороже переписывания одного файла);
|
||||||
@@ -730,7 +731,9 @@ pyrefly: в окружении нет ничего, кроме линтеров,
|
|||||||
опечатка, и три этих правила давали **311 срабатываний из 338** — шум, в котором
|
опечатка, и три этих правила давали **311 срабатываний из 338** — шум, в котором
|
||||||
тонут остальные 27.
|
тонут остальные 27.
|
||||||
|
|
||||||
**HH. `av-dev-backlog` исключён из проверки.** Плагин помечен устаревшим и живёт
|
**HH. `av-dev-backlog` исключён из проверки.** *(исчерпано темой 30: плагин
|
||||||
|
удалён, исключение снято из `pyproject.toml` и `copies.py`.)* Плагин помечен
|
||||||
|
устаревшим и живёт
|
||||||
до перевода последнего проекта, после чего удаляется целиком. Шесть его находок
|
до перевода последнего проекта, после чего удаляется целиком. Шесть его находок
|
||||||
косметические (`os.replace`, `l` как имя), а правка замороженного кода без
|
косметические (`os.replace`, `l` как имя), а правка замороженного кода без
|
||||||
тестов — риск без выгоды. Исключение уходит вместе с плагином.
|
тестов — риск без выгоды. Исключение уходит вместе с плагином.
|
||||||
@@ -1028,7 +1031,7 @@ HTML-комментарии, невидимые в отрендеренном ma
|
|||||||
|
|
||||||
**AAA. Ребро — это порядок, никогда не данные.** В обычном графе задач ребро
|
**AAA. Ребро — это порядок, никогда не данные.** В обычном графе задач ребро
|
||||||
тянет за собой вывод предшественника; здесь это запрещено: проход, увидевший
|
тянет за собой вывод предшественника; здесь это запрещено: проход, увидевший
|
||||||
чужие находки, соглашается с ними, и декорреляция — вся ценность конвейера —
|
чужие находки, соглашается с ними, и разведённость — вся ценность конвейера —
|
||||||
обнуляется. Сказано в самом правиле, потому что графовый словарь провоцирует
|
обнуляется. Сказано в самом правиле, потому что графовый словарь провоцирует
|
||||||
ровно эту ошибку. Исключение одно и оно же сток: триаж.
|
ровно эту ошибку. Исключение одно и оно же сток: триаж.
|
||||||
|
|
||||||
@@ -1728,3 +1731,530 @@ ADR, запискам разведки и сообщениям коммитов
|
|||||||
Из пяти языковых находок три приняты сразу, две отклонены — и обе отклонённые
|
Из пяти языковых находок три приняты сразу, две отклонены — и обе отклонённые
|
||||||
указывали на одно и то же место канона (правило 4 без оговорки). Вкусовых
|
указывали на одно и то же место канона (правило 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` /
|
- `canon` — привести проект к канону документов: `check` / `adopt` /
|
||||||
`upgrade`, плюс скрипт `docs.py`. Там же живёт язык проектных текстов —
|
`upgrade`, плюс скрипт `docs.py`. Там же живёт язык проектных текстов —
|
||||||
информационный стиль, англицизмы, жаргон;
|
информационный стиль, англицизмы, жаргон. Смысловую часть, которой скрипт
|
||||||
|
не видит, судят два агента: `doc-consistency` (документы между собой и с
|
||||||
|
openspec) и `doc-code-drift` (документы против кода);
|
||||||
- `docs` — содержимое канона по ходу разработки: ADR из архивного
|
- `docs` — содержимое канона по ходу разработки: ADR из архивного
|
||||||
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
|
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
|
||||||
архитектуры;
|
архитектуры;
|
||||||
- `tasks` — задачи и цели каталогом markdown-файлов; вычитывают их два
|
- `tasks` — задачи и цели каталогом markdown-файлов, у каждой записи тип
|
||||||
отдельных прохода: `task-form` (форма записи) и `doc-wording` (язык);
|
(`goal`, `feature`, `fix`, `chore`, `research`), и тип задаёт её схему;
|
||||||
|
вычитывают их два отдельных прохода: `task-form` (форма записи) и
|
||||||
|
`doc-wording` (язык);
|
||||||
- `session` — ритуал между спринтами и ведение спринта.
|
- `session` — ритуал между спринтами и ведение спринта.
|
||||||
- **av-dev-pipeline** — исполнение. **Требует OpenSpec.**
|
- **av-dev-pipeline** — исполнение. **Требует OpenSpec.**
|
||||||
- `task-pipeline` — задача через полный цикл SDD, от постановки до коммита;
|
- `task-pipeline` — задача через полный цикл SDD, от постановки до коммита;
|
||||||
@@ -29,8 +33,6 @@
|
|||||||
архитектура, обязательный триаж. Девять агентов-проходов, четыре ступени
|
архитектура, обязательный триаж. Девять агентов-проходов, четыре ступени
|
||||||
стоимости: `quick`, `standard`, `wide`, `deep`.
|
стоимости: `quick`, `standard`, `wide`, `deep`.
|
||||||
- **av-dev-git** — `commit`: сообщения в личном стиле.
|
- **av-dev-git** — `commit`: сообщения в личном стиле.
|
||||||
- **av-dev-backlog** — **устарел**, заменён `av-dev-pm`. Живёт до перевода
|
|
||||||
последнего проекта; как снять с проекта — [Снятие](#снятие).
|
|
||||||
|
|
||||||
Соглашение об именах: имя **плагина** длинное с префиксом `av-dev-`, имена
|
Соглашение об именах: имя **плагина** длинное с префиксом `av-dev-`, имена
|
||||||
**скилов** внутри — короткие. Вызов выходит вида `/av-dev-<плагин>:<скилл>`.
|
**скилов** внутри — короткие. Вызов выходит вида `/av-dev-<плагин>:<скилл>`.
|
||||||
@@ -68,29 +70,17 @@ flowchart TB
|
|||||||
## Канон документов проекта
|
## Канон документов проекта
|
||||||
|
|
||||||
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
|
Все проекты приводятся к одной раскладке — так проще ориентироваться, когда
|
||||||
проектов много, и рядом OpenSpec тоже держит строгую структуру. Определение —
|
проектов много, и рядом OpenSpec тоже держит строгую структуру. `CLAUDE.md` плюс
|
||||||
[av-dev-pm/skills/canon/references/canon.md](av-dev-pm/skills/canon/references/canon.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).
|
[project-facts.md](av-dev-pipeline/skills/review-pipeline/references/project-facts.md).
|
||||||
|
|
||||||
Прийти в старый проект и перевести его на канон — `/av-dev-pm:canon`. Канон
|
Прийти в старый проект и перевести его на канон — `/av-dev-pm:canon`. Канон
|
||||||
@@ -191,25 +181,25 @@ EOF
|
|||||||
|
|
||||||
## Снятие
|
## Снятие
|
||||||
|
|
||||||
Действие, обратное подключению. Актуально для `av-dev-backlog`: плагин устарел,
|
Действие, обратное подключению.
|
||||||
и с каждого проекта снимается по мере перевода задач на канон `docs/tasks/`.
|
|
||||||
|
|
||||||
**Сначала перевод, потом снятие.** Задачи переводит `/av-dev-pm:canon`
|
|
||||||
(`docs/backlog/` → `docs/tasks/`). Снять плагин раньше — остаться со старой
|
|
||||||
раскладкой и без скилла, который её понимает.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/project
|
cd /path/to/project
|
||||||
claude plugin uninstall av-dev-backlog@av-dev-skills --scope project
|
claude plugin uninstall <плагин>@av-dev-skills --scope project
|
||||||
```
|
```
|
||||||
|
|
||||||
Команда правит два места: убирает строку из `enabledPlugins` в
|
Команда правит два места: убирает строку из `enabledPlugins` в
|
||||||
`.claude/settings.json` проекта и запись из реестра
|
`.claude/settings.json` проекта и запись из реестра
|
||||||
`~/.claude/plugins/installed_plugins.json`. Снимок в
|
`~/.claude/plugins/installed_plugins.json`. Снимок в
|
||||||
`~/.claude/plugins/cache/av-dev-skills/av-dev-backlog/<версия>/` не трогает — он
|
`~/.claude/plugins/cache/av-dev-skills/<плагин>/<версия>/` не трогает — он
|
||||||
общий для всех проектов. Маркетплейс тоже остаётся: `extraKnownMarketplaces`
|
общий для всех проектов. Маркетплейс тоже остаётся: `extraKnownMarketplaces`
|
||||||
нужен остальным плагинам.
|
нужен остальным плагинам.
|
||||||
|
|
||||||
|
**Удалять из маркетплейса можно и до снятия с проектов.** `uninstall` идёт по
|
||||||
|
реестру, а не по `marketplace.json`, и снимает плагин, записи о котором в
|
||||||
|
манифесте уже нет. Проверено на `av-dev-backlog`: удалён из маркетплейса,
|
||||||
|
снят с jellybit после — команда отработала штатно.
|
||||||
|
|
||||||
`--scope project` обязателен по той же причине, что и при установке: умолчание у
|
`--scope project` обязателен по той же причине, что и при установке: умолчание у
|
||||||
команды — user. `cd` в проект обязателен, но здесь ошибка слышна — вызванная не
|
команды — user. `cd` в проект обязателен, но здесь ошибка слышна — вызванная не
|
||||||
оттуда, команда откажется словами `is not installed in project scope`, а не
|
оттуда, команда откажется словами `is not installed in project scope`, а не
|
||||||
@@ -254,10 +244,6 @@ uv run pyrefly check # типы
|
|||||||
линтеров, и любой сторонний импорт у него не разрешается. Список запретов —
|
линтеров, и любой сторонний импорт у него не разрешается. Список запретов —
|
||||||
не перечень мира, настоящий страж второй.
|
не перечень мира, настоящий страж второй.
|
||||||
|
|
||||||
`av-dev-backlog` из проверки исключён намеренно: плагин помечен устаревшим и
|
|
||||||
живёт до перевода последнего проекта, после чего удаляется целиком. Правки в
|
|
||||||
замороженный код — риск без выгоды.
|
|
||||||
|
|
||||||
## Проверка фронтматтеров
|
## Проверка фронтматтеров
|
||||||
|
|
||||||
Фронтматтер читает не человек, а загрузчик: по `name` он разрешает вызов, по
|
Фронтматтер читает не человек, а загрузчик: по `name` он разрешает вызов, по
|
||||||
@@ -273,7 +259,8 @@ uv run python scripts/frontmatter.py # 0 в порядке, 1 расхожд
|
|||||||
- **двоеточие с пробелом в описании без кавычек.** Для YAML `: ` внутри
|
- **двоеточие с пробелом в описании без кавычек.** Для YAML `: ` внутри
|
||||||
простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт,
|
простого скаляра начинает вложенное отображение, так что «конвейер ревью: гейт,
|
||||||
сверка…» — это не текст с двоеточием, а синтаксическая ошибка. Так было
|
сверка…» — это не текст с двоеточием, а синтаксическая ошибка. Так было
|
||||||
написано три описания из четырнадцати, и читались они правильно;
|
написано часть описаний плагинов, и читались они правильно — замер и разбор
|
||||||
|
в [DECISIONS.md](DECISIONS.md), решение III;
|
||||||
- **`name`, разошедшееся с именем каталога скилла или файла charter'а.** Вызов
|
- **`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).
|
План работ — [TODO.md](TODO.md). Решения с причинами — [DECISIONS.md](DECISIONS.md).
|
||||||
Здесь то, что **не** является работой из плана: незакрытые риски, честно принятые
|
Здесь то, что **не** является работой из плана: незакрытые риски, честно принятые
|
||||||
@@ -9,16 +10,16 @@
|
|||||||
|
|
||||||
## Главный незакрытый риск
|
## Главный незакрытый риск
|
||||||
|
|
||||||
**Калибровка не сделана, а charter'ы переписаны трижды.**
|
**Калибровка не сделана, а уставы проходов с тех пор переписывались не раз.**
|
||||||
|
|
||||||
Первый раз девять charter'ов правили при выносе в плагин: предмет проверки
|
Правки шли волнами: вынос в плагин (предмет проверки заменён ссылкой на раздел
|
||||||
заменили ссылкой на раздел брифа. `references/calibration.md` требует при такой
|
брифа), переход на пути документов канона, две правки по находкам ревью, граф
|
||||||
правке замерить, помогла ли она, — **замера не было**. Второй раз их переписали
|
порядка, ступень `wide`, пересмотр триггеров ступени.
|
||||||
коммитом `9cef452`: ссылка на раздел брифа заменена путём документа канона.
|
`references/calibration.md` требует при каждой такой правке замерить, помогла ли
|
||||||
Третий — коммитами `0eab075` и следующим, по находкам ревью: `adversary`, `ops`,
|
она, — **ни одного замера не было**. Числа правок здесь нет намеренно: счётчик
|
||||||
`reimpl`, `rubric` и `triage` правились ещё раз.
|
пришлось бы двигать вручную, и он уже однажды отстал.
|
||||||
|
|
||||||
**Три неизмеренных изменения подряд** в том самом месте, где присваивается
|
**Неизмеренные изменения копятся** в том самом месте, где присваивается
|
||||||
severity. Пробы готовы и синтетических не нужно — четыре реальные находки
|
severity. Пробы готовы и синтетических не нужно — четыре реальные находки
|
||||||
прошедшей сессии healthlog:
|
прошедшей сессии healthlog:
|
||||||
|
|
||||||
@@ -35,17 +36,19 @@ severity. Пробы готовы и синтетических не нужно
|
|||||||
но работает ли обязанность, не проверено. Оркестратор реагирует на severity,
|
но работает ли обязанность, не проверено. Оркестратор реагирует на severity,
|
||||||
поэтому цена — не «не найдём», а **«найдём и не починим»**.
|
поэтому цена — не «не найдём», а **«найдём и не починим»**.
|
||||||
|
|
||||||
Замер стоит перед переездом jellybit и блокирует его (решение 39).
|
Сама работа — [TODO.md](TODO.md), раздел 3; здесь только цена: замер стоит
|
||||||
|
перед переездом jellybit и блокирует его (решение 39), а ожидаемый исход уже
|
||||||
|
назван выше.
|
||||||
|
|
||||||
|
**«Главный» здесь про цену, а не про очередь.** Первой идёт адаптация healthlog
|
||||||
|
(TODO, раздел 2): без неё нет проекта под каноном, на котором работают остальные
|
||||||
|
скиллы. Калибровка блокирует один шаг — переезд jellybit, — а не всё подряд.
|
||||||
|
|
||||||
## Что ещё не сделано
|
## Что ещё не сделано
|
||||||
|
|
||||||
Список работ — в [TODO.md](TODO.md). Здесь только то, что стоит держать в голове
|
Список работ — в [TODO.md](TODO.md). Здесь только то, что стоит держать в голове
|
||||||
отдельно:
|
отдельно:
|
||||||
|
|
||||||
- **Плагины отправлены, но ни к одному проекту не подключены.** 17 коммитов
|
|
||||||
ушли на origin, клон маркетплейса обновлён до `88c5d97` и видит `av-dev-pm`
|
|
||||||
и `av-dev-pipeline` — то есть подключать теперь есть что. Первым делом это
|
|
||||||
делает healthlog, по разделу 2 плана.
|
|
||||||
- **Ни один скилл не прогонялся на живом проекте.** `docs.py` прогнан на
|
- **Ни один скилл не прогонялся на живом проекте.** `docs.py` прогнан на
|
||||||
healthlog и jellybit в режиме `check` и находит осмысленный дрейф; `init`,
|
healthlog и jellybit в режиме `check` и находит осмысленный дрейф; `init`,
|
||||||
`canon adopt`, `canon upgrade` и скилл `docs` не исполнялись ни разу.
|
`canon adopt`, `canon upgrade` и скилл `docs` не исполнялись ни разу.
|
||||||
@@ -57,18 +60,38 @@ severity. Пробы готовы и синтетических не нужно
|
|||||||
|
|
||||||
## Открытые вопросы
|
## Открытые вопросы
|
||||||
|
|
||||||
|
**`doc-consistency` не различает «про нас» и «про то, что мы производим».**
|
||||||
|
Первый прогон на самом dev-skills предъявил репозиторию правило из
|
||||||
|
`av-dev-git/skills/commit/SKILL.md` — а это продукт, уезжающий в чужие проекты,
|
||||||
|
а не правило, которому подчиняется маркетплейс. На проекте под каноном такой
|
||||||
|
путаницы нет (там документы описывают сам проект), поэтому в устав это пока не
|
||||||
|
дописано: сперва посмотреть, встретится ли класс ещё раз.
|
||||||
|
|
||||||
**Как проверять, что канон не разошёлся с проектами после `upgrade`.** `canon
|
**Как проверять, что канон не разошёлся с проектами после `upgrade`.** `canon
|
||||||
check` сверяет версию, но не то, что миграционные записи journal'а применены
|
check` сверяет версию, но не то, что миграционные записи journal'а применены
|
||||||
верно. Проект может нести `"canon": 2` и не иметь того, что версия 2 требовала.
|
верно. Проект может нести `"canon": 2` и не иметь того, что версия 2 требовала.
|
||||||
|
Ответ выбран: шагом 6 `upgrade` зовутся оба судьи документов — проверка не
|
||||||
|
механическая, но других у существа записей нет. Останется открытым, пока не
|
||||||
|
прогнано на живом проекте: неизвестно, ловят ли они недоделанную миграцию или
|
||||||
|
только её последствия.
|
||||||
|
|
||||||
**Форма ADR при пересмотре решения.** Правило «старая запись получает статус
|
**Не выродились ли «границы покрытия» в шаблон.** Строка «что смотрели и чего не
|
||||||
`заменено на`» требует, чтобы кто-то заметил, что новое решение отменяет старое.
|
смотрели» обязательна в докладе `check`, `adopt`, интейка, штурма, сессии,
|
||||||
Механической проверки нет, а принуждённое отрицание на шаге синка спрашивает про
|
спринта и всех четырёх агентов — семь и больше раз за сессию, и проверить её
|
||||||
`adr/` вообще, а не «не отменяет ли это что-то из существующего».
|
исполнение некому: приёмщик и исполнитель одно лицо (`session/SKILL.md`,
|
||||||
|
«Стимулы»). Выродившаяся строка **хуже отсутствия**: доклад выглядит проверенным.
|
||||||
|
|
||||||
**Что делать с `av-dev-backlog` после перевода jellybit.** Помечен устаревшим и
|
Приём не правится: это гипотеза об износе, а не находка, и менять работающее по
|
||||||
переписан так, чтобы не ловить триггер. Удалять его из маркетплейса или оставить
|
догадке дороже. **Наблюдение к первой обкатке на живом проекте:** если в трёх
|
||||||
как есть — решится, когда jellybit переедет.
|
докладах подряд границы покрытия совпали дословно или называют не то, чего
|
||||||
|
проверка действительно не касалась, — приём выродился, и вот тогда решать.
|
||||||
|
|
||||||
|
**Форма ADR при пересмотре решения.** Парный статус («старая запись получает
|
||||||
|
`заменено на`») судит агент `doc-consistency` — правило 6 его устава. Охват был
|
||||||
|
открытым вопросом, пока агент зовётся пачкой, отобранной работой; переезд вызова
|
||||||
|
на сессию с пачкой «весь канон» его снял. Остаётся зазор в спринт и отсутствие
|
||||||
|
механической проверки — то есть пересмотр, сделанный сегодня, судится на
|
||||||
|
ближайшей сессии, а не в момент правки.
|
||||||
|
|
||||||
## Известные пределы — приняты, чинить не планируется
|
## Известные пределы — приняты, чинить не планируется
|
||||||
|
|
||||||
|
|||||||
@@ -116,9 +116,10 @@
|
|||||||
- [ ] `canon adopt`; `docs/backlog/` → `docs/tasks/`
|
- [ ] `canon adopt`; `docs/backlog/` → `docs/tasks/`
|
||||||
- [ ] `architecture.md` 1662 строки → обзор, остаток маркерами (W)
|
- [ ] `architecture.md` 1662 строки → обзор, остаток маркерами (W)
|
||||||
- [ ] после выноса поведения — замерить остаток `architecture.md` и решить по
|
- [ ] после выноса поведения — замерить остаток `architecture.md` и решить по
|
||||||
каталожной форме: жмёт → канон версии **4** для `architecture.md` и
|
каталожной форме: жмёт → **следующая** версия канона для
|
||||||
`review.md`, точка входа `README.md` (тема 16, GGG, 65; версию 3 занял
|
`architecture.md` и `review.md`, точка входа `README.md` (тема 16, GGG,
|
||||||
роадмап с родом работы, тема 17, 68)
|
65; версию 3 занял роадмап с родом работы, тема 17, 68; версию 4 —
|
||||||
|
секция `Сопровождение` и порядок секций, тема 26)
|
||||||
- [ ] завести `security.md` с периметром первой строкой (J)
|
- [ ] завести `security.md` с периметром первой строкой (J)
|
||||||
- [ ] `review-journal.md` → `review.md` + настройка конвейера (K, L)
|
- [ ] `review-journal.md` → `review.md` + настройка конвейера (K, L)
|
||||||
- [ ] `conventions.md` → `conventions/`, `local-research.md` → `research/` (G)
|
- [ ] `conventions.md` → `conventions/`, `local-research.md` → `research/` (G)
|
||||||
@@ -150,11 +151,11 @@
|
|||||||
- [ ] `docs/specs/architecture.md` → `docs/architecture.md`, `database.md` →
|
- [ ] `docs/specs/architecture.md` → `docs/architecture.md`, `database.md` →
|
||||||
`docs/database.md`, `jellyfin-layout.md` → `docs/research/`
|
`docs/database.md`, `jellyfin-layout.md` → `docs/research/`
|
||||||
- [ ] `docs/review/journal.md` → `docs/review.md`
|
- [ ] `docs/review/journal.md` → `docs/review.md`
|
||||||
- [ ] `drafts/` растворить: roadmap → `ROADMAP.md`, conventions-backlog → задачи
|
- [ ] `drafts/` растворить: roadmap → `ROADMAP.md`, conventions-backlog → записи
|
||||||
`[idea]`, logical-title-model → ADR (H)
|
`research` (сырьё: тип есть, «Вопрос» пуст), logical-title-model → ADR (H)
|
||||||
- [ ] `docs/backlog/` → `docs/tasks/`
|
- [ ] `docs/backlog/` → `docs/tasks/`
|
||||||
- [ ] удалить проектные копии скиллов и агентов (4 из REMAINING)
|
- [ ] удалить проектные копии скиллов и агентов (4 из REMAINING)
|
||||||
- [ ] `av-dev-backlog` удалить из маркетплейса
|
- [x] `av-dev-backlog` удалить из маркетплейса и снять с проекта (тема 30)
|
||||||
|
|
||||||
## 6. Канон версии 3 — повысить живые проекты (тема 17)
|
## 6. Канон версии 3 — повысить живые проекты (тема 17)
|
||||||
|
|
||||||
@@ -162,22 +163,48 @@
|
|||||||
jellybit 43. Шаги повышения — [changelog.md](av-dev-pm/skills/canon/references/changelog.md),
|
jellybit 43. Шаги повышения — [changelog.md](av-dev-pm/skills/canon/references/changelog.md),
|
||||||
запись «Версия 3»; делаются скиллом `av-dev-pm:canon` в режиме `upgrade`.
|
запись «Версия 3»; делаются скиллом `av-dev-pm:canon` в режиме `upgrade`.
|
||||||
|
|
||||||
- [ ] healthlog: `PLAN.md` → `ROADMAP.md`, ссылки, `"canon": 3`
|
- [x] healthlog: `PLAN.md` → `ROADMAP.md`, ссылки, `"canon": 3` — сделано,
|
||||||
|
лежит в рабочем дереве проекта некоммитнутым
|
||||||
- [ ] jellybit: то же
|
- [ ] jellybit: то же
|
||||||
- [ ] род работы и раздел «Затрагивает» — **не задним числом**: сперва то, что
|
- [ ] род работы и раздел «Затрагивает» — **не задним числом**: сперва то, что
|
||||||
идёт в ближайший набор (`sprint take` без них откажет), остальное по ходу
|
идёт в ближайший набор (`sprint take` без них откажет), остальное по ходу
|
||||||
переоценки (PPP)
|
переоценки (PPP)
|
||||||
- [ ] секции роадмапа: `порядок` → `Запланировано`, `темы` → `Направления`,
|
- [ ] секции роадмапа: `порядок` → `Запланировано`, `темы` → `Направления`,
|
||||||
завести `Готово` и `Разработка`; прозаические разделы healthlog («Что уже
|
завести `Готово` и `Сопровождение`; прозаические разделы healthlog («Что уже
|
||||||
пройдено», «Почему в таком порядке») разложить — звенья строками в
|
пройдено», «Почему в таком порядке») разложить — звенья строками в
|
||||||
`Готово`, обоснование очереди прозой внутри `Запланировано` (тема 19, 80).
|
`Готово`, обоснование очереди прозой внутри `Запланировано` (тема 19, 80).
|
||||||
`check` теперь называет чужую секцию ошибкой, так что шаг обязателен
|
`check` теперь называет чужую секцию ошибкой, так что шаг обязателен
|
||||||
- [ ] переформулировать цели ответом на «что приложение будет уметь»; цели не
|
- [ ] переформулировать цели ответом на «что приложение будет уметь»; цели не
|
||||||
про приложение («Процесс и качество разработки» в jellybit) — в
|
про приложение («Процесс и качество разработки» в jellybit) — в
|
||||||
`Разработка`
|
`Сопровождение`
|
||||||
- [ ] `check --fix` на обоих: поднимет написание канонических секций, поставит
|
- [ ] `check --fix` на обоих: поднимет написание канонических секций, поставит
|
||||||
отбивку после заголовков и сведёт секцию в мете файлов с заголовками.
|
отбивку после заголовков и сведёт секцию в мете файлов с заголовками.
|
||||||
Секции беклога переименовать руками — имена выбирал проект (тема 20, ККК)
|
Секции беклога переименовать руками — имена выбирал проект (тема 20, ККК)
|
||||||
- [ ] заголовки задач в форму действия — **не задним числом**: по мере попадания
|
- [ ] заголовки задач в форму действия — **не задним числом**: по мере попадания
|
||||||
задачи в работу. `check` печатает их число, `task-form` предложит
|
задачи в работу. `check` печатает их число, `task-form` предложит
|
||||||
формулировки пачкой (тема 20, ЕЕЕ)
|
формулировки пачкой (тема 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`**;
|
пункт из раздела инвариантов `CLAUDE.md`**;
|
||||||
- сослаться на наблюдение в `docs/research/` — оно сильнее любого
|
- сослаться на наблюдение в `docs/research/` — оно сильнее любого
|
||||||
рассуждения о том, «как должно быть».
|
рассуждения о том, «как должно быть».
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ description: "Конвейер ревью изменения — детерми
|
|||||||
заданный критерий) и **generative** (сперва порождают критерий или
|
заданный критерий) и **generative** (сперва порождают критерий или
|
||||||
альтернативу, потом сравнивают). Расширять чек-листы бесполезно; неявный слой
|
альтернативу, потом сравнивают). Расширять чек-листы бесполезно; неявный слой
|
||||||
достают только generative-проходы.
|
достают только generative-проходы.
|
||||||
2. **Ценность верификатора = наличие внешнего оракула × декорреляция с
|
2. **Ценность верификатора = наличие внешнего оракула × разведённость с
|
||||||
автором**, а не число ролей. Под всеми ролями одна модель с одними
|
автором**, а не число ролей. Под всеми ролями одна модель с одними
|
||||||
априорными, вход у всех общий: седьмая роль почти не добавляет recall, но
|
априорными, вход у всех общий: седьмая роль почти не добавляет recall, но
|
||||||
линейно удорожает триаж. Иерархия надёжности: детерминированный инструмент >
|
линейно удорожает триаж. Иерархия надёжности: детерминированный инструмент >
|
||||||
@@ -352,7 +352,7 @@ flowchart TD
|
|||||||
**Ребро значит «A закончил раньше, чем B стартовал», и ничего больше.** В обычном
|
**Ребро значит «A закончил раньше, чем B стартовал», и ничего больше.** В обычном
|
||||||
графе задач ребро тянет за собой данные — здесь нет, и это не деталь реализации.
|
графе задач ребро тянет за собой данные — здесь нет, и это не деталь реализации.
|
||||||
Проход **не видит** находок других проходов, в каком бы порядке их ни запустили.
|
Проход **не видит** находок других проходов, в каком бы порядке их ни запустили.
|
||||||
Вся ценность конвейера держится на декорреляции: под всеми ролями одна модель с
|
Вся ценность конвейера держится на разведённости: под всеми ролями одна модель с
|
||||||
одними априорными, и стоит показать ей чужой вывод — она согласится. Согласие
|
одними априорными, и стоит показать ей чужой вывод — она согласится. Согласие
|
||||||
нескольких проходов и так не повышает `confidence` (см. «Честный предел»);
|
нескольких проходов и так не повышает `confidence` (см. «Честный предел»);
|
||||||
согласие **наведённое** ещё и маскируется под независимое подтверждение.
|
согласие **наведённое** ещё и маскируется под независимое подтверждение.
|
||||||
@@ -671,7 +671,7 @@ flowchart TD
|
|||||||
Третий шаг обязателен.
|
Третий шаг обязателен.
|
||||||
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
|
- Дефект, проскочивший ревью и всплывший позже, идёт в журнал проекта
|
||||||
([references/review-journal.md](references/review-journal.md)) — сразу, не
|
([references/review-journal.md](references/review-journal.md)) — сразу, не
|
||||||
ретроспективно: теряется именно причина непоймания.
|
ретроспективно: теряется именно то, почему дефект не поймали.
|
||||||
- **Отчёт триажа сохраняется вместе с изменением** — `openspec/changes/<id>/review/`.
|
- **Отчёт триажа сохраняется вместе с изменением** — `openspec/changes/<id>/review/`.
|
||||||
Он единственное, по чему потом видно, что было найдено и что из этого не
|
Он единственное, по чему потом видно, что было найдено и что из этого не
|
||||||
заведено: нулевой урожай при непустом отчёте виден сразу.
|
заведено: нулевой урожай при непустом отчёте виден сразу.
|
||||||
@@ -688,7 +688,7 @@ flowchart TD
|
|||||||
туториальному: отсюда тяга к интерфейсам ради интерфейсов, лишним мокам и
|
туториальному: отсюда тяга к интерфейсам ради интерфейсов, лишним мокам и
|
||||||
конфигурируемости, которую никто не просил. **«Идиоматично» и «распространено» —
|
конфигурируемости, которую никто не просил. **«Идиоматично» и «распространено» —
|
||||||
разные вещи**; проходы обязаны различать их и опираться на поимённое положение
|
разные вещи**; проходы обязаны различать их и опираться на поимённое положение
|
||||||
гайда, а не на ощущение частотности.
|
руководства, а не на ощущение частотности.
|
||||||
|
|
||||||
Согласие нескольких проходов — **не подтверждение**: это один источник,
|
Согласие нескольких проходов — **не подтверждение**: это один источник,
|
||||||
высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`.
|
высказавшийся несколько раз. Совпадение повышает приоритет, но не `confidence`.
|
||||||
@@ -699,11 +699,12 @@ flowchart TD
|
|||||||
Независимо от проекта недоступно:
|
Независимо от проекта недоступно:
|
||||||
|
|
||||||
- поведение внешних систем в их будущих версиях;
|
- поведение внешних систем в их будущих версиях;
|
||||||
- реальный профиль нагрузки и то, что на самом деле лежит в данных;
|
- реальный профиль нагрузки; и то, что на самом деле лежит в данных, — **сверх
|
||||||
|
того, что снято с провенансом в `docs/research/`**;
|
||||||
- завязка внешних потребителей на текущую форму ответа;
|
- завязка внешних потребителей на текущую форму ответа;
|
||||||
- суждение «этой функциональности не должно существовать».
|
- суждение «этой функциональности не должно существовать».
|
||||||
|
|
||||||
Отдельно и честно: **поимённая сверка с положениями стайлгайдов языка не
|
Отдельно и честно: **поимённая сверка с положениями руководств по стилю языка не
|
||||||
задаётся ни одним проходом.** Проход про идиоматичность упразднён, его способные
|
задаётся ни одним проходом.** Проход про идиоматичность упразднён, его способные
|
||||||
части переселены (эксперимент против поведения библиотеки и драйвера — в `ops`,
|
части переселены (эксперимент против поведения библиотеки и драйвера — в `ops`,
|
||||||
вопрос 8; «не изобретаем ли то, что уже есть в библиотеке» — в `architecture`,
|
вопрос 8; «не изобретаем ли то, что уже есть в библиотеке» — в `architecture`,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- Файл: internal/<пакет>/<файл>.go:120-134
|
- Файл: internal/<пакет>/<файл>.go:120-134
|
||||||
- Severity: critical | major | minor | nit
|
- Severity: critical | major | minor | nit
|
||||||
- Confidence: high | medium | low
|
- Confidence: high | medium | low
|
||||||
- Оракул: <падающий тест / команда с выводом / положение гайда / нет>
|
- Оракул: <падающий тест / команда с выводом / положение руководства / нет>
|
||||||
- Последствие: <что произойдёт и при каких условиях>
|
- Последствие: <что произойдёт и при каких условиях>
|
||||||
- Предложение: <конкретное изменение>
|
- Предложение: <конкретное изменение>
|
||||||
- Найдено проходом: <имя агента>
|
- Найдено проходом: <имя агента>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
Симптом в заголовке — это заявка на то, что читатель сам достроит последствие;
|
Симптом в заголовке — это заявка на то, что читатель сам достроит последствие;
|
||||||
он не достроит, он просто починит симптом.
|
он не достроит, он просто починит симптом.
|
||||||
- **`critical` без оракула или построенного пути не существует.** Оракул — это
|
- **`critical` без оракула или построенного пути не существует.** Оракул — это
|
||||||
падающий тест, вывод выполненной команды или поимённое положение гайда. Не
|
падающий тест, вывод выполненной команды или поимённое положение руководства. Не
|
||||||
«вероятно, здесь гонка», а прогон детектора гонок с его выводом.
|
«вероятно, здесь гонка», а прогон детектора гонок с его выводом.
|
||||||
- **`confidence: low` — это «так обычно пишут».** Такие находки допустимы, но не
|
- **`confidence: low` — это «так обычно пишут».** Такие находки допустимы, но не
|
||||||
поднимаются выше `minor`. Частотность конструкции в публичном коде — не
|
поднимаются выше `minor`. Частотность конструкции в публичном коде — не
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ flowchart TD
|
|||||||
теряет связность;
|
теряет связность;
|
||||||
- правило переезжает в **перечень механизированного в
|
- правило переезжает в **перечень механизированного в
|
||||||
`docs/conventions/README.md`** — со ссылкой на место механизации: конфиг
|
`docs/conventions/README.md`** — со ссылкой на место механизации: конфиг
|
||||||
линтера, собственный анализатор, тест-сканер исходников. Непойманное место
|
линтера, собственный анализатор, тест-сканер исходников. Не названное место
|
||||||
означает, что проход будет добросовестно проверять уже проверенное;
|
означает, что проход будет добросовестно проверять уже проверенное;
|
||||||
- из контекста инструмента спек убирается дубль, если он там был.
|
- из контекста инструмента спек убирается дубль, если он там был.
|
||||||
|
|
||||||
@@ -100,8 +100,8 @@ Charter'ы проходов при этом **не правятся**: они о
|
|||||||
принципиально не выражается правилом.** Файл конвенций на несколько сотен строк
|
принципиально не выражается правилом.** Файл конвенций на несколько сотен строк
|
||||||
размазывает внимание модели по тривиальному — она добросовестно проверит
|
размазывает внимание модели по тривиальному — она добросовестно проверит
|
||||||
именование полей лога и не дойдёт до формы решения. Каждая строка конвенций,
|
именование полей лога и не дойдёт до формы решения. Каждая строка конвенций,
|
||||||
которую можно было бы проверить машиной, оплачивается непойманным дефектом
|
которую можно было бы проверить машиной, оплачивается дефектом, который не
|
||||||
где-то ещё.
|
поймали где-то ещё.
|
||||||
|
|
||||||
## Обратное движение
|
## Обратное движение
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
Артефакт проекта, а не плагина: файл живёт в репозитории — **`docs/review.md`**,
|
Артефакт проекта, а не плагина: файл живёт в репозитории — **`docs/review.md`**,
|
||||||
слот канона `av-dev-pm`. Здесь описано, зачем он и какой формы, потому что без
|
слот канона `av-dev-pm`. Здесь описано, зачем он и какой формы, потому что без
|
||||||
него конвейер не учится: находки закрываются, причины непоймания теряются, и один
|
него конвейер не учится: находки закрываются, а почему их не поймали — забывается,
|
||||||
и тот же класс проскакивает второй раз.
|
и один и тот же класс проскакивает второй раз.
|
||||||
|
|
||||||
Тот же файл держит **настройку конвейера под проект** — типовые узлы, типовые
|
Тот же файл держит **настройку конвейера под проект** — типовые узлы, типовые
|
||||||
ложноположительные, вопросы к проходам, недоступно проверке. Это не соседство по
|
ложноположительные, вопросы к проходам, недоступно проверке. Это не соседство по
|
||||||
@@ -12,12 +12,12 @@
|
|||||||
## Что туда попадает
|
## Что туда попадает
|
||||||
|
|
||||||
**Воспроизведённый дефект — с пометкой `проскочил` или `пойман ревью`.**
|
**Воспроизведённый дефект — с пометкой `проскочил` или `пойман ревью`.**
|
||||||
Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а
|
Записывается **сразу**, а не ретроспективно: со временем теряется не сам факт, а то,
|
||||||
причина непоймания — единственное, ради чего журнал существует.
|
почему дефект не поймали, — единственное, ради чего журнал существует.
|
||||||
|
|
||||||
Пометка делит журнал на две выборки с разным назначением:
|
Пометка делит журнал на две выборки с разным назначением:
|
||||||
|
|
||||||
- **проскочил** — эвал-сет для калибровки конвейера. Реальный промах сильнее
|
- **проскочил** — проверочный набор для калибровки конвейера. Реальный промах сильнее
|
||||||
синтетической пробы: синтетические смещены в сторону тех, которые уже умеешь
|
синтетической пробы: синтетические смещены в сторону тех, которые уже умеешь
|
||||||
придумывать;
|
придумывать;
|
||||||
- **пойман ревью** — прецеденты с оракулом. Самая сильная опора, какая у прохода
|
- **пойман ревью** — прецеденты с оракулом. Самая сильная опора, какая у прохода
|
||||||
|
|||||||
@@ -88,8 +88,9 @@ description: Проводит несколько задач разом — пл
|
|||||||
### 1. Прочитать набор
|
### 1. Прочитать набор
|
||||||
|
|
||||||
Набор задан списком (слаги, файлы, описания) — прочитай файл каждой задачи и
|
Набор задан списком (слаги, файлы, описания) — прочитай файл каждой задачи и
|
||||||
связанные спеки и черновики. Задачи-идеи включаются, но помни: сабагент проведёт
|
связанные спеки и черновики. Сырьё (в терминах `av-dev-pm` — запись типа
|
||||||
их сперва через `opsx:explore`, это тяжелее и чаще упирается в вопрос.
|
`research` с пустым разделом «Вопрос») включается, но помни: сабагент проведёт
|
||||||
|
его сперва через `opsx:explore`, это тяжелее и чаще упирается в вопрос.
|
||||||
|
|
||||||
### 2. Спланировать порядок и пересечения (автономно)
|
### 2. Спланировать порядок и пересечения (автономно)
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ flowchart TD
|
|||||||
тем же charter'ам `av-dev-pipeline`, сохранив обязательное — гейт до
|
тем же 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
|
name: doc-wording
|
||||||
description: "Вычитка языка проектных текстов по информационному стилю — документы канона, решения ADR, записки разведки, задачи и цели, вперемешку тоже. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта. Отдаёт готовые формулировки на замену и ничего не правит сам. Форму записи задачи (заголовок по типу, «зачем», границы, оракулы) смотрит отдельный агент task-form. Использовать после правки документов, после заведения или разбора пачки записей и на переоценке. Только чтение."
|
description: "Вычитка языка проектных текстов по информационному стилю — документы канона, решения ADR, записки разведки, задачи и цели, вперемешку тоже. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта, транслит в имени файла. Отдаёт готовые формулировки на замену и ничего не правит сам. Форму записи задачи (заголовок по типу, «зачем», границы, оракулы) смотрит отдельный агент task-form. Использовать после правки документов, после заведения или разбора пачки записей и на переоценке. Только чтение."
|
||||||
tools: Read, Grep, Glob
|
tools: Read, Grep, Glob
|
||||||
model: sonnet
|
model: sonnet
|
||||||
color: green
|
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 -->
|
<!-- копия: язык-жаргон из av-dev-pm/skills/canon/references/language.md -->
|
||||||
|
|
||||||
@@ -114,7 +142,7 @@ color: green
|
|||||||
|
|
||||||
<!-- /копия: язык-жаргон -->
|
<!-- /копия: язык-жаргон -->
|
||||||
|
|
||||||
7. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
8. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||||
употребляется.** Заменять его своей догадкой нельзя: ты не знаешь предметную
|
употребляется.** Заменять его своей догадкой нельзя: ты не знаешь предметную
|
||||||
область. Пиши «термин «X» не встречается ни в документах, ни в других
|
область. Пиши «термин «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` и
|
**Машинной проверке — вообще ничего.** Всё, что ловят `tasks.py check` и
|
||||||
`docs.py check` (состав и написание секций, наличие разделов, число критериев,
|
`docs.py check` (состав и написание секций, наличие разделов, число критериев,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: task-form
|
name: task-form
|
||||||
description: "Проверка формы записи каталога задач по существу: форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, идея — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент doc-wording. Использовать после заведения или разбора пачки записей, до взятия в спринт и на переоценке. Только чтение."
|
description: "Проверка формы записи каталога задач по существу: тип, разошедшийся с содержанием записи, форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, разведка — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент doc-wording. Использовать после заведения или разбора пачки записей, до взятия в спринт и на переоценке. Только чтение."
|
||||||
tools: Read, Grep, Glob
|
tools: Read, Grep, Glob
|
||||||
model: opus
|
model: opus
|
||||||
color: yellow
|
color: yellow
|
||||||
@@ -29,7 +29,7 @@ color: yellow
|
|||||||
|
|
||||||
Список файлов записей (`docs/tasks/items/<slug>.md`) или каталог задач целиком.
|
Список файлов записей (`docs/tasks/items/<slug>.md`) или каталог задач целиком.
|
||||||
Каталог тебе нужен и сам по себе: задача несёт тег `goal:<слаг>`, и **файл цели
|
Каталог тебе нужен и сам по себе: задача несёт тег `goal:<слаг>`, и **файл цели
|
||||||
ты открываешь**, иначе шестое правило не проверить.
|
ты открываешь**, иначе седьмое правило не проверить.
|
||||||
|
|
||||||
Документы проекта — паспорт, архитектура, конвенции — если зовущий их назвал.
|
Документы проекта — паспорт, архитектура, конвенции — если зовущий их назвал.
|
||||||
По ним видно, названа ли граница именем, которое в проекте существует.
|
По ним видно, названа ли граница именем, которое в проекте существует.
|
||||||
@@ -38,11 +38,14 @@ color: yellow
|
|||||||
|
|
||||||
1. **Заголовок отвечает на вопрос своего типа.**
|
1. **Заголовок отвечает на вопрос своего типа.**
|
||||||
|
|
||||||
|
Тип стоит первым полем меты — `- **Тип:** …`, — а в заголовке ему
|
||||||
|
соответствует эмодзи.
|
||||||
|
|
||||||
| Тип | Отвечает на | Форма |
|
| Тип | Отвечает на | Форма |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `[goal]` | что приложение будет уметь | утверждение о возможности: «Соперником может быть компьютер» |
|
| 🎯 `goal` | что приложение будет уметь | утверждение о возможности: «Соперником может быть компьютер» |
|
||||||
| задача | что нужно сделать | глагол в неопределённой форме, допускается «не» перед ним: «Печатать поле одним куском кода» |
|
| ✨ `feature`, 🐞 `fix`, 🧹 `chore` | что нужно сделать | глагол в неопределённой форме, допускается «не» перед ним: «Печатать поле одним куском кода» |
|
||||||
| `[idea]` | о чём она | назывное, без обещания: «Подсказка следующего хода» |
|
| 🔬 `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`, тебе оно неинтересно.
|
`tasks.py check`, тебе оно неинтересно.
|
||||||
|
|
||||||
5. **Предписания процесса в теле нет.** «Делать профилем standard», «взять
|
6. **Предписания процесса в теле нет.** «Делать профилем standard», «взять
|
||||||
такой-то агент» — это выбор, который делают, увидев изменение, а не при
|
такой-то агент» — это выбор, который делают, увидев изменение, а не при
|
||||||
постановке. Он же путь понизить требования решением, принятым до
|
постановке. Он же путь понизить требования решением, принятым до
|
||||||
проектирования.
|
проектирования.
|
||||||
|
|
||||||
6. **Задача называет, какую строку «Завершения» своей цели она двигает.**
|
7. **Задача называет, какую строку «Завершения» своей цели она двигает.**
|
||||||
Открой файл цели из тега `goal:<слаг>` и сверь. Три исхода, и все три —
|
Открой файл цели из тега `goal:<слаг>` и сверь. Три исхода, и все три —
|
||||||
разные находки:
|
разные находки:
|
||||||
|
|
||||||
@@ -94,16 +117,18 @@ color: yellow
|
|||||||
это незакрытая часть возможности. Скажи о ней отдельно, вне списка находок
|
это незакрытая часть возможности. Скажи о ней отдельно, вне списка находок
|
||||||
по файлам: это про набор, а не про запись.
|
по файлам: это про набор, а не про запись.
|
||||||
|
|
||||||
У задачи **без цели** (`kind:fix`, `chore`, `research`) правило не
|
У задачи **без цели** (`fix`, `chore`, `research`) правило не применяется
|
||||||
применяется вовсе — они служат работоспособности, а не направлению.
|
вовсе — они служат работоспособности, а не направлению.
|
||||||
|
|
||||||
## Чего ты не проверяешь
|
## Чего ты не проверяешь
|
||||||
|
|
||||||
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
||||||
|
|
||||||
**Чужому подрядчику — строкой в границах покрытия.** Язык у `doc-wording`;
|
**Чужому подрядчику — строкой в границах покрытия.** Язык у `doc-wording`;
|
||||||
увидел — назови в конце одной строкой, чтобы находка не пропала, но находкой не
|
согласованность документов канона между собой у `doc-consistency`, их
|
||||||
оформляй.
|
соответствие коду у `doc-code-drift` — до задач эти двое не доходят вовсе, но
|
||||||
|
если ты открыл цель и увидел расхождение в самом документе, оно их. Увидел —
|
||||||
|
назови в конце одной строкой, чтобы находка не пропала, но находкой не оформляй.
|
||||||
|
|
||||||
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (наличие
|
**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (наличие
|
||||||
разделов, число критериев, состав и написание секций, теги, тег `question` при
|
разделов, число критериев, состав и написание секций, теги, тег `question` при
|
||||||
@@ -113,7 +138,7 @@ color: yellow
|
|||||||
одного правила.
|
одного правила.
|
||||||
|
|
||||||
**Содержание работы**: нужна ли задача, верно ли выбрана цель, не крупна ли она,
|
**Содержание работы**: нужна ли задача, верно ли выбрана цель, не крупна ли она,
|
||||||
достаточна ли декомпозиция. Шестое правило подходит к этому близко и
|
достаточна ли декомпозиция. Седьмое правило подходит к этому близко и
|
||||||
останавливается там, где кончается сверка с текстом цели. Об этом молчи.
|
останавливается там, где кончается сверка с текстом цели. Об этом молчи.
|
||||||
|
|
||||||
## Порог вмешательства
|
## Порог вмешательства
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ description: Привести проект к канону документов
|
|||||||
которое прочитали последним. Прочитай его **до** первой правки.
|
которое прочитали последним. Прочитай его **до** первой правки.
|
||||||
|
|
||||||
- [references/skeletons.md](references/skeletons.md) — **что именно класть** в
|
- [references/skeletons.md](references/skeletons.md) — **что именно класть** в
|
||||||
каждый незаполненный слот. Своей формой заглушку не выдумывай: `docs.py`
|
каждый незаполненный слот. Не выдумывай заглушку своей формы: `docs.py`
|
||||||
узнаёт только плейсхолдер `<!-- заполнить: … -->` из шаблонов.
|
узнаёт только плейсхолдер `<!-- заполнить: … -->` из шаблонов.
|
||||||
- [references/language.md](references/language.md) — **как это написано словами**:
|
- [references/language.md](references/language.md) — **как это написано словами**:
|
||||||
информационный стиль, применённый к проектным текстам, таблицы англицизмов и
|
информационный стиль, применённый к проектным текстам, таблицы англицизмов и
|
||||||
@@ -67,23 +67,29 @@ python3 $ds version --dir <корень> # версия кано
|
|||||||
capability: незаполненный канон это переходное состояние, а не отказ. Маркеры
|
capability: незаполненный канон это переходное состояние, а не отказ. Маркеры
|
||||||
долга просто считает числом.
|
долга просто считает числом.
|
||||||
|
|
||||||
**Ты** судишь о том, чего она не умеет:
|
Того, чего она не умеет, **ты не судишь сам** — для этого есть два агента, и
|
||||||
|
разведены они по глубине:
|
||||||
|
|
||||||
- **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что
|
| Агент | Что смотрит | Читает |
|
||||||
capability `recognition`. Файлы разные, содержание одно;
|
| --- | --- | --- |
|
||||||
- **поведение, оставшееся в `architecture.md`** — раздел на 900 строк с
|
| `doc-consistency` | смысловой дубль, прямое противоречие между документами, поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса, число без провенанса, заглушка вместо честной строки | `docs/`, `openspec/` |
|
||||||
требованиями вместо обзора;
|
| `doc-code-drift` | протухший факт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки проекта, capability | весь репозиторий |
|
||||||
- **достаточность честной строки** — «внешних зависимостей нет» это факт,
|
|
||||||
«TBD» — пробел;
|
Судит **не тот, кто писал**: самопроверка документа слабее всего ровно там, где
|
||||||
- **протухший факт** — документ ссылается на то, чего в коде уже нет.
|
формулировка казалась удачной при написании. Ни один из них ничего не правит —
|
||||||
|
оба возвращают готовые формулировки, подставляешь ты.
|
||||||
|
|
||||||
## `check`
|
## `check`
|
||||||
|
|
||||||
1. `docs.py check`, при наличии базы диффа — с `--base`.
|
1. `docs.py check`, при наличии базы диффа — с `--base`.
|
||||||
2. Прочитай то, что скрипт проверить не может (список выше), по документам,
|
2. **Агентов на каждом `check` не зови.** Оба — `doc-consistency` и
|
||||||
которых касалась работа. Не «заодно по всему `docs/`».
|
`doc-code-drift` — зовутся раз в спринт (шаг сессии), а также шагом 6 `adopt`
|
||||||
3. Доклад: вывод скрипта строкой исхода, твои находки поимённо, **граница
|
и шагом 6 `upgrade`, на весь канон разом. Они дороги: оба на `opus`, второй
|
||||||
покрытия** — что смотрел и чего не смотрел.
|
ещё и читает репозиторий. Позвал `doc-code-drift` — передай ему раздел
|
||||||
|
запретов `CLAUDE.md`.
|
||||||
|
3. Доклад: вывод скрипта строкой исхода, находки агентов поимённо, **граница
|
||||||
|
покрытия** — что смотрели и чего не смотрели, и **кого из двоих позвал**:
|
||||||
|
доклад, умолчавший об этом, читается как «сверено».
|
||||||
|
|
||||||
Дрейф раскладки чинится переносом; смысловые находки — это либо правка
|
Дрейф раскладки чинится переносом; смысловые находки — это либо правка
|
||||||
документа, либо задача, если работы больше чем на абзац.
|
документа, либо задача, если работы больше чем на абзац.
|
||||||
@@ -95,7 +101,7 @@ capability: незаполненный канон это переходное с
|
|||||||
`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый
|
`docs.py check` — он уже назовёт упразднённые слоты с адресом, куда каждый
|
||||||
уезжает. **Но смотрит он только верхний уровень `docs/`:** упразднённое в корне
|
уезжает. **Но смотрит он только верхний уровень `docs/`:** упразднённое в корне
|
||||||
репозитория (`BRIEF.md`) и во вложенных каталогах он не назовёт никогда, поэтому
|
репозитория (`BRIEF.md`) и во вложенных каталогах он не назовёт никогда, поэтому
|
||||||
корневые `*.md` читаются глазами. Плюс: `CLAUDE.md`, `openspec/specs/` (список
|
корневые `*.md` читай глазами. Плюс: `CLAUDE.md`, `openspec/specs/` (список
|
||||||
capability), `openspec/config.yaml`.
|
capability), `openspec/config.yaml`.
|
||||||
|
|
||||||
### 2. Составь карту
|
### 2. Составь карту
|
||||||
@@ -131,8 +137,8 @@ capability), `openspec/config.yaml`.
|
|||||||
незаполненное — одной честной информативной строкой, а не «TBD»;
|
незаполненное — одной честной информативной строкой, а не «TBD»;
|
||||||
3. переносы содержимого;
|
3. переносы содержимого;
|
||||||
4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он
|
4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он
|
||||||
владеет форматом задач, включая переименование транслитных слагов в
|
владеет форматом задач. Он же переименует транслитные слаги в английские и
|
||||||
английские вместе с починкой перекрёстных ссылок;
|
тем же проходом починит перекрёстные ссылки;
|
||||||
5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
|
5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
|
||||||
`CLAUDE.md`, `README.md`;
|
`CLAUDE.md`, `README.md`;
|
||||||
6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
|
6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
|
||||||
@@ -161,6 +167,20 @@ capability), `openspec/config.yaml`.
|
|||||||
содержания, сколько маркеров долга в `architecture.md`, сколько задач без
|
содержания, сколько маркеров долга в `architecture.md`, сколько задач без
|
||||||
критериев приёмки. Закрывается порциями по ходу работы, а не одним заходом.
|
критериев приёмки. Закрывается порциями по ходу работы, а не одним заходом.
|
||||||
|
|
||||||
|
### 6. Позови обоих судей
|
||||||
|
|
||||||
|
`docs.py` увидел раскладку, а не смысл: перенос растащил один факт по двум домам,
|
||||||
|
оставил в `architecture.md` поведение, которому место в спеке, и оторвал ADR от
|
||||||
|
его `design.md`. Ничего из этого скрипт не видит, и первый прогон на живом
|
||||||
|
проекте обычно самый урожайный — правило единственного дома до адаптации никто не
|
||||||
|
проверял.
|
||||||
|
|
||||||
|
Зови **`doc-consistency`** (документы между собой и с openspec) и
|
||||||
|
**`doc-code-drift`** (факты против кода). Разбирай порциями, а не одним заходом.
|
||||||
|
|
||||||
|
**Передай им объявленное переходное состояние из шага 5** — иначе честная строка
|
||||||
|
в незаполненном слоте вернётся находкой, а это не поломка, а объявленный долг.
|
||||||
|
|
||||||
## `upgrade` — канон вырос
|
## `upgrade` — канон вырос
|
||||||
|
|
||||||
1. `docs.py version` — версия проекта и версия скрипта.
|
1. `docs.py version` — версия проекта и версия скрипта.
|
||||||
@@ -171,10 +191,21 @@ capability), `openspec/config.yaml`.
|
|||||||
применяются по порядку.
|
применяются по порядку.
|
||||||
4. Подними `canon` в `docs/.pm.json` до текущей.
|
4. Подними `canon` в `docs/.pm.json` до текущей.
|
||||||
5. `docs.py check`.
|
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`
|
Это **единственный дом определения канона**. Скиллы `init`, `canon` и `docs`
|
||||||
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
|
читают его, а не пересказывают: три описания одной раскладки разъедутся, и
|
||||||
@@ -22,6 +22,29 @@
|
|||||||
[language.md](language.md): информационный стиль, англицизмы, жаргон. Он
|
[language.md](language.md): информационный стиль, англицизмы, жаргон. Он
|
||||||
относится и к задачам, и к решениям ADR, и к запискам разведки.
|
относится и к задачам, и к решениям ADR, и к запискам разведки.
|
||||||
|
|
||||||
|
## Сопровождение и эксплуатация — целое и часть
|
||||||
|
|
||||||
|
Одна тема живёт в трёх местах канона, и путать их слова нельзя.
|
||||||
|
|
||||||
|
**Сопровождение** — всё, чем держат проект: инструмент и сборка, процесс,
|
||||||
|
выкладка, метрики и логи, инфраструктура, дежурство. **Эксплуатация** — его
|
||||||
|
часть: работа системы на проде. Целое и часть, и никогда наоборот.
|
||||||
|
|
||||||
|
| Место | Уровень | Что там |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ROADMAP.md`, секция `Сопровождение` | план | **работы**, которые собираемся делать: цели и их задачи |
|
||||||
|
| `architecture.md`, раздел «Эксплуатация» | состояние | **как устроено сейчас**: где работает, что рядом, кто перезапускает |
|
||||||
|
| эксплуатационный проход ревью | оптика | **чем проверяем**: «это упало через неделю на проде» |
|
||||||
|
|
||||||
|
Слово **«поддержка» не употребляется вовсе** — в нём слышится помощь
|
||||||
|
пользователю, а это другая работа.
|
||||||
|
|
||||||
|
**Граница с возможностями проходит по тому, кто наблюдает.** «Приложение
|
||||||
|
сообщает о своём состоянии» — возможность приложения, её место среди прочих
|
||||||
|
целей: наблюдает пользователь сервиса. «Дежурный видит состояние на одном
|
||||||
|
экране» — сопровождение: наблюдаем мы. Одни и те же метрики попадают в разные
|
||||||
|
секции роадмапа, и это верно — секции отвечают на разные вопросы.
|
||||||
|
|
||||||
## Раскладка
|
## Раскладка
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -35,10 +58,10 @@ docs/
|
|||||||
security.md периметр; недоверенный вход; что вне модели
|
security.md периметр; недоверенный вход; что вне модели
|
||||||
conventions/
|
conventions/
|
||||||
README.md индекс, правило промоута, что механизировано
|
README.md индекс, правило промоута, что механизировано
|
||||||
<тема>.md
|
<slug>.md
|
||||||
research/
|
research/
|
||||||
README.md как снималось, индекс
|
README.md как снималось, индекс
|
||||||
<тема>.md наблюдения и числа с провенансом
|
<slug>.md наблюдения и числа с провенансом
|
||||||
adr/
|
adr/
|
||||||
README.md индекс записей, статусы, правило замены
|
README.md индекс записей, статусы, правило замены
|
||||||
template.md
|
template.md
|
||||||
@@ -52,8 +75,29 @@ openspec/
|
|||||||
changes/archive/ архив изменений с design.md — сырьё для ADR
|
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` держит
|
Прозой остаётся **только то, что не выражается правилом**. `README.md` держит
|
||||||
индекс, правило промоута и **перечень уже механизированного** со ссылкой на
|
индекс, правило промоута и **перечень уже механизированного** со ссылкой на
|
||||||
место механизации — конфиг линтера, собственный анализатор, тест-сканер
|
место механизации — конфиг линтера, собственный анализатор, тест-сканер
|
||||||
исходников. Непойманное место механизации означает, что проход добросовестно
|
исходников. Не названное место механизации означает, что проход добросовестно
|
||||||
проверит уже проверенное.
|
проверит уже проверенное.
|
||||||
|
|
||||||
### `research/`
|
### `research/`
|
||||||
@@ -199,7 +243,7 @@ kebab-case.
|
|||||||
проверять сознательно» (пересматривается первым).
|
проверять сознательно» (пересматривается первым).
|
||||||
|
|
||||||
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
|
**Журнал дефектов:** запись на каждый воспроизведённый дефект с пометкой
|
||||||
**проскочил / пойман ревью**. Проскочившие — эвал-сет для калибровки конвейера,
|
**проскочил / пойман ревью**. Проскочившие — проверочный набор для калибровки конвейера,
|
||||||
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
|
выборка по пометке. Пойманные с оракулом — лучшая опора для прохода: проектные,
|
||||||
воспроизводимые, однажды оказавшиеся правдой.
|
воспроизводимые, однажды оказавшиеся правдой.
|
||||||
|
|
||||||
@@ -217,18 +261,36 @@ kebab-case.
|
|||||||
становится: нормативное поведение живёт в `openspec/specs/`, роадмап отвечает,
|
становится: нормативное поведение живёт в `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`
|
### `CLAUDE.md`
|
||||||
|
|
||||||
@@ -262,6 +324,7 @@ kebab-case.
|
|||||||
|
|
||||||
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
|
Факт живёт ровно в одном файле; остальные ссылаются. Карта на случай спора:
|
||||||
|
|
||||||
|
<!-- дом: карта-домов -->
|
||||||
| Факт | Дом |
|
| Факт | Дом |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| поведение системы | `openspec/specs/<capability>/spec.md` |
|
| поведение системы | `openspec/specs/<capability>/spec.md` |
|
||||||
@@ -276,6 +339,7 @@ kebab-case.
|
|||||||
| единые точки проекта | `architecture.md` |
|
| единые точки проекта | `architecture.md` |
|
||||||
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
|
| имя основной ветки, `testdata`, временный каталог | `CLAUDE.md` |
|
||||||
| что уже механизировано правилом | `conventions/README.md` |
|
| что уже механизировано правилом | `conventions/README.md` |
|
||||||
|
<!-- /дом: карта-домов -->
|
||||||
|
|
||||||
## Пустое называется пустым
|
## Пустое называется пустым
|
||||||
|
|
||||||
@@ -301,7 +365,7 @@ kebab-case.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
|
| `docs/review-brief.md` | документы канона и есть бриф; остаток — в `review.md` |
|
||||||
| `docs/specs/` | `openspec/specs/` (поведение) и `architecture.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` |
|
| `docs/plan.md` | `docs/tasks/ROADMAP.md` |
|
||||||
| `BRIEF.md` | `passport.md` |
|
| `BRIEF.md` | `passport.md` |
|
||||||
| `docs/backlog/` | `docs/tasks/` |
|
| `docs/backlog/` | `docs/tasks/` |
|
||||||
@@ -312,22 +376,43 @@ kebab-case.
|
|||||||
Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон
|
Граница объявляется вслух в каждом отчёте: `check`, отчитавшийся «канон
|
||||||
соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего.
|
соблюдён» на проекте, где из шести файлов три лишние, хуже отсутствующего.
|
||||||
|
|
||||||
| Проверяет `docs.py` | Судит агент |
|
| Проверяет `docs.py` | Судит агент | Какой |
|
||||||
| --- | --- |
|
| --- | --- | --- |
|
||||||
| отсутствующие пути канона | смысловой дубль документа и capability |
|
| отсутствующие пути канона | смысловой дубль документа и capability | `doc-consistency` |
|
||||||
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` |
|
| файлы в `docs/` вне канона | поведение, оставшееся в `architecture.md` | `doc-consistency` |
|
||||||
| битые относительные ссылки | протухший факт, разошедшийся с кодом |
|
| имя файла не kebab-case латиницей; форма имени ADR | транслит в имени — сверх эвристики | `doc-wording` |
|
||||||
| версия канона и её отставание | достаточность честной строки в пустом слоте |
|
| битые относительные ссылки | прямое противоречие между документами | `doc-consistency` |
|
||||||
| нетронутый плейсхолдер шаблона | связность и читаемость |
|
| версия канона и её отставание | достаточность честной строки в пустом слоте | `doc-consistency` |
|
||||||
| маркеры долга — числом | |
|
| нетронутый плейсхолдер шаблона | ADR без ссылки на `design.md`, замена без парного статуса | `doc-consistency` |
|
||||||
| миграция изменена, а `database.md` нет | |
|
| маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` |
|
||||||
| capability без упоминания в `architecture.md` | |
|
| миграция изменена, а `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`
|
## `docs/.pm.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"canon": 2,
|
"canon": 4,
|
||||||
"migrations": "internal/store/migrations",
|
"migrations": "internal/store/migrations",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"backlog": "INDEX.md"
|
"backlog": "INDEX.md"
|
||||||
@@ -341,10 +426,14 @@ kebab-case.
|
|||||||
`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего
|
`database.md`. `tasks` — настройки каталога задач, переехавшие сюда из прежнего
|
||||||
`<tasks>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
|
`<tasks>/.tasks.json`: **один конфиг на весь канон, а не по одному на каталог**.
|
||||||
Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`,
|
Внутри `tasks` — **только имена файлов и заголовков** (`items`, `backlog`,
|
||||||
`plan`, `sprint`, `rejected`, `sprint_section`, `questions_heading`,
|
`roadmap`, `sprint`, `rejected`, `sprint_section`, `oracle_word` и заголовки
|
||||||
`criteria_heading`, `oracle_word`), и ключ пишется, лишь когда имя отличается от
|
разделов тела: `criteria_heading`, `surface_heading`, `questions_heading`,
|
||||||
умолчания. **Секций беклога здесь нет:** их дом — заголовки `##` самого индекса,
|
`completion_heading`, `repro_heading`, `question_heading`, `answer_heading`,
|
||||||
и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py`
|
`scope_heading`), и ключ пишется, лишь когда имя отличается от умолчания.
|
||||||
|
**Словаря типов здесь нет** — он закрыт каноном, а не настраивается проектом:
|
||||||
|
настраиваемый словарь типов разъехался бы на синонимах ровно так же, как
|
||||||
|
открытый. **Категорий беклога здесь тоже нет:** их дом — заголовки `##` самого
|
||||||
|
индекса, и второй список сразу разошёлся бы с первым. Неизвестный ключ `tasks.py`
|
||||||
отвергает кодом 3, поэтому лишнее слово в этом объекте останавливает работу с
|
отвергает кодом 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
|
## Версия 3 — 2026-08-04
|
||||||
|
|
||||||
Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность
|
Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность
|
||||||
@@ -92,7 +214,9 @@ upgrade` идёт по записям снизу вверх от версии п
|
|||||||
идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю
|
идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю
|
||||||
частоту полного набора уточнением.
|
частоту полного набора уточнением.
|
||||||
7. Переименовать секции роадмапа: `порядок` → `Запланировано`, `темы` →
|
7. Переименовать секции роадмапа: `порядок` → `Запланировано`, `темы` →
|
||||||
`Направления`; завести `Готово` **первой** и `Разработка` последней.
|
`Направления`; завести `Готово` **первой** и `Разработка` последней
|
||||||
|
(порядок секций поменялся в версии 4 — если едешь сразу на неё, заводи
|
||||||
|
`Готово` последней и не переставляй дважды).
|
||||||
Прозаические разделы вроде «Что уже пройдено», которые велись руками,
|
Прозаические разделы вроде «Что уже пройдено», которые велись руками,
|
||||||
разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно),
|
разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно),
|
||||||
обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе
|
обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе
|
||||||
|
|||||||
@@ -124,6 +124,38 @@
|
|||||||
|
|
||||||
<!-- /дом: язык-англицизмы -->
|
<!-- /дом: язык-англицизмы -->
|
||||||
|
|
||||||
|
## Свой словарь — закрытый список
|
||||||
|
|
||||||
|
Слово, не переводимое потому, что оно **имя вещи этого процесса**, а не украшение.
|
||||||
|
Оговорка «термин прижился» без списка проверяема на глаз и потому не проверяема:
|
||||||
|
прижившимся выглядит любое слово, встреченное трижды.
|
||||||
|
|
||||||
|
<!-- дом: язык-словарь -->
|
||||||
|
| Термин | Что называет |
|
||||||
|
| --- | --- |
|
||||||
|
| интейк | заведение записи с фильтром и дедупом: «заведение» называет создание файла, слить их — смешать две операции |
|
||||||
|
| триаж | ступень конвейера, сводящая находки в решение |
|
||||||
|
| провенанс | обязательное свойство числа: чем и при каких условиях получено. «Источник» рядом называет саму запись, а не свойство |
|
||||||
|
| дедуп, дедупликация | сверка нового против уже лежащего |
|
||||||
|
| чек-лист | перечень, по которому идут сверху вниз, называя исход каждой строки |
|
||||||
|
| дифф, `--base` | разница между состояниями в git |
|
||||||
|
| промпт | текст, которым зовут модель |
|
||||||
|
| change, capability, spec | сущности OpenSpec, имена вещей чужого инструмента |
|
||||||
|
| generative, applicative | роды проходов ревью, вводятся определением по месту |
|
||||||
|
|
||||||
|
**Список закрыт.** Слово не отсюда и не из таблицы имён вещей выше — находка, а
|
||||||
|
не «принятый стиль»: у него либо есть живой русский аналог, либо оно требует
|
||||||
|
ввода одной строкой при первом употреблении.
|
||||||
|
|
||||||
|
Отсюда же читается снятое. Эти слова из текстов убраны, и возвращать их не надо:
|
||||||
|
**конфляция** (смешение), **декорреляция** (разведённость, разведён с кем-то),
|
||||||
|
**непоймание** (почему не поймали), **эвал-сет** (проверочный набор), **гайд**
|
||||||
|
(руководство). Каждое было латинизмом или калькой при живом русском слове, и
|
||||||
|
каждое к моменту снятия жило в трёх-шести файлах разом — то есть выглядело
|
||||||
|
словарём, не будучи им.
|
||||||
|
|
||||||
|
<!-- /дом: язык-словарь -->
|
||||||
|
|
||||||
## Жаргон и метафоры
|
## Жаргон и метафоры
|
||||||
|
|
||||||
Система не описывается внутренними метафорами и образными ярлыками: автору они
|
Система не описывается внутренними метафорами и образными ярлыками: автору они
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
`adr/README.md` и `review.md` уезжают в репозиторий проекта и обязаны там что-то
|
`adr/README.md` и `review.md` уезжают в репозиторий проекта и обязаны там что-то
|
||||||
говорить; определение при этом остаётся в [canon.md](canon.md). Отсюда
|
говорить; определение при этом остаётся в [canon.md](canon.md). Отсюда
|
||||||
обязанность: **правка такого правила в каноне тянет запись в
|
обязанность: **правка такого правила в каноне тянет запись в
|
||||||
[changelog.md](changelog.md)** с указанием, какой файл проекта поднимает
|
[changelog.md](changelog.md)** — и запись называет, какой файл проекта поднимает
|
||||||
`upgrade`. Без этого копия в проекте останется на старой версии молча.
|
`upgrade`. Без этого копия в проекте останется на старой версии молча.
|
||||||
|
|
||||||
**Каждая такая копия помечена и сверяется машиной.** Дом обрамляется
|
**Каждая такая копия помечена и сверяется машиной.** Дом обрамляется
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
| Правило | Где механизировано |
|
| Правило | Где механизировано |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|
||||||
Непойманное место механизации означает, что проход по конвенциям будет
|
Не названное здесь место механизации означает, что проход по конвенциям будет
|
||||||
добросовестно проверять уже проверенное.
|
добросовестно проверять уже проверенное.
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -216,8 +216,9 @@
|
|||||||
|
|
||||||
## Соглашения
|
## Соглашения
|
||||||
|
|
||||||
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, слаг английский, дата — когда решение
|
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение реально принято.
|
||||||
реально принято.
|
Слаг **английский по сути, а не транслитом**: `queue-as-table`, не
|
||||||
|
`ochered-tablicej`. Форму имени и слаг проверяет `docs.py check`.
|
||||||
- Записи неизменяемы: передумали — новая запись, старой ставится статус.
|
- Записи неизменяемы: передумали — новая запись, старой ставится статус.
|
||||||
- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и
|
- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и
|
||||||
`устарело`; ставятся полем меты записи — `- **Статус:** …` рядом с датой и
|
`устарело`; ставятся полем меты записи — `- **Статус:** …` рядом с датой и
|
||||||
@@ -307,7 +308,7 @@
|
|||||||
## Журнал дефектов
|
## Журнал дефектов
|
||||||
|
|
||||||
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
|
Запись на каждый воспроизведённый дефект, сразу, а не ретроспективно: со
|
||||||
временем теряется не факт, а причина непоймания.
|
временем теряется не факт, а то, почему дефект не поймали.
|
||||||
|
|
||||||
Форма:
|
Форма:
|
||||||
|
|
||||||
@@ -392,7 +393,7 @@ severity стоит здесь, а не выводится каждым прох
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"canon": 2
|
"canon": 4
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from dataclasses import dataclass, field
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
|
||||||
CANON_VERSION = 3
|
CANON_VERSION = 4
|
||||||
|
|
||||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
@@ -70,11 +70,110 @@ RETIRED = {
|
|||||||
"local-research.md": "→ docs/research/",
|
"local-research.md": "→ docs/research/",
|
||||||
"research.md": "→ docs/research/",
|
"research.md": "→ docs/research/",
|
||||||
"specs": "поведение → openspec/specs/, обзор → docs/architecture.md",
|
"specs": "поведение → openspec/specs/, обзор → docs/architecture.md",
|
||||||
"drafts": "идея → задача [idea], отказ → ADR, порядок → ROADMAP.md",
|
"drafts": "идея → запись research, отказ → ADR, порядок → ROADMAP.md",
|
||||||
"backlog": "→ docs/tasks/",
|
"backlog": "→ docs/tasks/",
|
||||||
"review": "→ docs/review.md",
|
"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*-->")
|
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
|
||||||
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
|
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
|
||||||
MD_LINK = 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(f" {msg}")
|
||||||
|
|
||||||
print(
|
print(
|
||||||
"\nМашина проверила раскладку, ссылки, версию и две сверки с кодом.\n"
|
"\nМашина проверила раскладку, имена файлов, ссылки, версию и две сверки\n"
|
||||||
"Смысловые дубли, оставшееся в architecture.md поведение и достаточность\n"
|
"с кодом. Согласованность документов между собой и с кодом она не\n"
|
||||||
"честной строки в пустом слоте она не проверяет — это суждение агента."
|
"проверяет — это суждение агентов `doc-consistency` (документ ↔ документ\n"
|
||||||
|
"↔ openspec) и `doc-code-drift` (документ ↔ код)."
|
||||||
)
|
)
|
||||||
if rep.errors:
|
if rep.errors:
|
||||||
print(f"\nИтог: дрейф, {len(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_version(root, cfg, rep)
|
||||||
check_required(root, cfg, rep)
|
check_required(root, cfg, rep)
|
||||||
check_stray(root, rep)
|
check_stray(root, rep)
|
||||||
|
check_slugs(root, rep)
|
||||||
check_links(root, rep)
|
check_links(root, rep)
|
||||||
check_placeholders_and_debt(root, rep)
|
check_placeholders_and_debt(root, rep)
|
||||||
check_capabilities(root, rep)
|
check_capabilities(root, rep)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ description: Вести содержимое документов канона
|
|||||||
|
|
||||||
Причина, по которой правило именно такое, измерена: у ADR был список триггеров
|
Причина, по которой правило именно такое, измерена: у ADR был список триггеров
|
||||||
прозой — и он дал **6 записей на 43 изменения**. Прозаический триггер, который
|
прозой — и он дал **6 записей на 43 изменения**. Прозаический триггер, который
|
||||||
некому проверить, не срабатывает. Умолчание «не написал» становится отличимым от
|
некому проверить, не срабатывает. Отличить «не написал» от «написал, что не
|
||||||
«написал, что не требуется», только когда отрицание обязательно.
|
требуется» можно только тогда, когда отрицание обязательно.
|
||||||
|
|
||||||
Это тот же приём, что «границы покрытия» в отчёте ревью и «пустое называется
|
Это тот же приём, что «границы покрытия» в отчёте ревью и «пустое называется
|
||||||
пустым» в каноне.
|
пустым» в каноне.
|
||||||
@@ -50,11 +50,29 @@ description: Вести содержимое документов канона
|
|||||||
Синк документации:
|
Синк документации:
|
||||||
- architecture.md — добавлен воркер свёртки, ссылка на capability reindex
|
- architecture.md — добавлен воркер свёртки, ссылка на capability reindex
|
||||||
- database.md — миграция 00006, таблица bucket
|
- database.md — миграция 00006, таблица bucket
|
||||||
- adr/ — заведён ADR-2026-08-03-ochered-tablicej: отказ от внешней очереди
|
- adr/ — заведён ADR-2026-08-03-queue-as-table: отказ от внешней очереди
|
||||||
- research/ — новое о формате не узнано
|
- research/ — новое о формате не узнано
|
||||||
- passport, security, conventions, review — не требуется: изменение внутреннее
|
- passport, security, conventions, review — не требуется: изменение внутреннее
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Сверка — не здесь, а на сессии
|
||||||
|
|
||||||
|
Синк правит документы поодиночке, а расходятся они **между собой**: факт,
|
||||||
|
дописанный в `architecture.md`, уже живёт в `CLAUDE.md`; периметр в
|
||||||
|
`security.md` не знает про новый эндпоинт. Поймать это на своей же правке нельзя,
|
||||||
|
и судит это агент `doc-consistency`.
|
||||||
|
|
||||||
|
**Но синк его не зовёт.** Оба судьи документов — `doc-consistency` и
|
||||||
|
`doc-code-drift` — зовутся раз в спринт, шагом сессии, на весь канон разом.
|
||||||
|
Причина в цене: агент на `opus` по каждой сделанной задаче — самая дорогая
|
||||||
|
церемония процесса. К тому же расхождение между двумя документами по определению
|
||||||
|
требует двух документов, а на большинстве задач синк правит один.
|
||||||
|
|
||||||
|
Что теряется: привязка находки к задаче, которая её породила. Что выигрывается,
|
||||||
|
кроме денег: синк перестаёт отбирать пачку, и в неё попадают документы,
|
||||||
|
которых работа не касалась, — расхождение, внесённое правкой в одном месте, там
|
||||||
|
и живёт.
|
||||||
|
|
||||||
## ADR — промоут, а не второе сочинение
|
## ADR — промоут, а не второе сочинение
|
||||||
|
|
||||||
Обоснование уже написано: `opsx:propose` кладёт `design.md` в каждый change, и
|
Обоснование уже написано: `opsx:propose` кладёт `design.md` в каждый change, и
|
||||||
@@ -83,7 +101,7 @@ description: Вести содержимое документов канона
|
|||||||
маркера долга и правило «гейт от них не краснеет» — в
|
маркера долга и правило «гейт от них не краснеет» — в
|
||||||
[каноне](../canon/references/canon.md), раздел `architecture.md`.**
|
[каноне](../canon/references/canon.md), раздел `architecture.md`.**
|
||||||
|
|
||||||
Разбирается порциями: раздел вычищается той задачей, которая его касается.
|
Разбирается порциями: раздел вычищает та задача, которая его касается.
|
||||||
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
|
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
|
||||||
обоснование в ADR, обзор остаётся строкой со ссылкой на capability.
|
обоснование в ADR, обзор остаётся строкой со ссылкой на capability.
|
||||||
|
|
||||||
@@ -109,8 +127,8 @@ description: Вести содержимое документов канона
|
|||||||
формы взять негде.
|
формы взять негде.
|
||||||
|
|
||||||
Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
|
Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
|
||||||
Со временем теряется не факт, а причина непоймания — единственное, ради чего
|
Со временем теряется не факт, а то, почему дефект не поймали, — единственное,
|
||||||
журнал есть. И решение о сужении проверок (перестали звать проход, понизили
|
ради чего журнал есть. И решение сузить проверки (перестали звать проход, понизили
|
||||||
профиль) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
|
профиль) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
|
||||||
|
|
||||||
## Промоут в конвенции
|
## Промоут в конвенции
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ description: "Завести новый проект — сессия вопро
|
|||||||
Вход — свободный текст «что мне нужно и почему». Выход — канон документов, с
|
Вход — свободный текст «что мне нужно и почему». Выход — канон документов, с
|
||||||
которого дальше работают все остальные скиллы.
|
которого дальше работают все остальные скиллы.
|
||||||
|
|
||||||
**Определение канона — [канон](../canon/references/canon.md).** Читается до
|
**Определение канона — [канон](../canon/references/canon.md).** Прочитай его до
|
||||||
первого вопроса: интервью идёт по слотам канона, а не по вкусу. Что класть в
|
первого вопроса: интервью идёт по слотам канона, а не по вкусу. Что класть в
|
||||||
каждый файл — [скелеты](../canon/references/skeletons.md); своей формы заглушки
|
каждый файл — [скелеты](../canon/references/skeletons.md); не выдумывай заглушки
|
||||||
не выдумывай, `docs.py` узнаёт только плейсхолдер оттуда.
|
своей формы, `docs.py` узнаёт только плейсхолдер оттуда.
|
||||||
|
|
||||||
## Что `init` физически не может произвести
|
## Что `init` физически не может произвести
|
||||||
|
|
||||||
В новом репозитории **нет кода**, а `architecture.md`, `database.md`,
|
В новом репозитории **нет кода**, а `architecture.md`, `database.md`,
|
||||||
`conventions/` и `research/` выводятся из него. Их сочинение на старте — это
|
`conventions/` и `research/` выводятся из него. Сочинить их на старте — значит
|
||||||
проектирование вперёд реальности, и оно протухнет раньше первой задачи.
|
проектировать вперёд реальности, и написанное протухнет раньше первой задачи.
|
||||||
|
|
||||||
Поэтому `init` заполняет то, что человек знает **до первой строки кода**:
|
Поэтому `init` заполняет то, что человек знает **до первой строки кода**:
|
||||||
|
|
||||||
@@ -56,13 +56,13 @@ description: "Завести новый проект — сессия вопро
|
|||||||
|
|
||||||
- **Не больше трёх вопросов за итерацию** (`AskUserQuestion`), рекомендация
|
- **Не больше трёх вопросов за итерацию** (`AskUserQuestion`), рекомендация
|
||||||
первым вариантом. Между итерациями применяй уже решённое.
|
первым вариантом. Между итерациями применяй уже решённое.
|
||||||
- **Сперва вычитай ответы из брифа.** Вопрос, ответ на который в тексте уже
|
- **Сперва вычитай ответы из брифа.** Если ответ уже есть в тексте, вопрос не
|
||||||
есть, задавать не надо — покажи своё прочтение и спроси, верно ли.
|
задавай — покажи своё прочтение и спроси, верно ли.
|
||||||
- **Не выдумывай четыре вещи:** периметр, что необратимо, измеренные числа и
|
- **Не выдумывай четыре вещи:** периметр, что необратимо, измеренные числа и
|
||||||
адресата дорогой проверки. Их из замысла не вывести. Не сказано — пиши
|
адресата дорогой проверки. Их из замысла не вывести. Не сказано — пиши
|
||||||
«неизвестно» с пометкой, что ждёт ответа.
|
«неизвестно» с пометкой, что ждёт ответа.
|
||||||
- **Развилка замысла — человеку, механика — сама.** Имена файлов, слаги, порядок
|
- **Развилка замысла — человеку, механика — сама.** Имена файлов, слаги, порядок
|
||||||
строк не выносятся.
|
строк не выноси.
|
||||||
|
|
||||||
## Порядок работы
|
## Порядок работы
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: session
|
name: session
|
||||||
description: "Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги. Формат и содержимое задач — скилл tasks."
|
description: "Ритуал между спринтами и ведение самого спринта: разбор накопившихся вопросов, разбор прошедшего спринта про процесс, переоценка задач порциями, выбор цели и набор нового спринта с заморозкой. Плюс правила по ходу спринта — что врывается в замороженный набор, чем вопрос отличается от блокера, когда задача выходит из спринта, что считается сделанным и что идёт в доклад. Использовать, когда просят закрыть или начать спринт, собрать набор, разобрать вопросы, провести груминг/переоценку/ретроспективу, решить «что делать дальше» или доложить итоги, а также когда вернулись к проекту после перерыва и надо понять, где остановились. Формат и содержимое задач — скилл tasks."
|
||||||
---
|
---
|
||||||
|
|
||||||
# Сессия между спринтами
|
# Сессия между спринтами
|
||||||
@@ -37,8 +37,10 @@ description: "Ритуал между спринтами и ведение са
|
|||||||
|
|
||||||
## Единицы
|
## Единицы
|
||||||
|
|
||||||
- **Цель** — то, ради чего набирается спринт. Файл `[goal]`, перечисленный в
|
- **Цель** — то, ради чего набирается спринт. Файл типа `goal` (🎯),
|
||||||
`ROADMAP.md`. Цель постоянна: живёт, пока живёт направление.
|
перечисленный в `ROADMAP.md`. Цель постоянна: живёт, пока живёт направление, —
|
||||||
|
и уходит вместе с ним, если замысел оказался неверен (порядок отмены — в
|
||||||
|
[tasks](../tasks/references/task-goal.md#отменённая-цель--сперва-задачи-потом-цель)).
|
||||||
- **Задача** — то, что мерджится целиком и даёт видимую пользу.
|
- **Задача** — то, что мерджится целиком и даёт видимую пользу.
|
||||||
- **Вопрос** — решение человека. Не останавливает начатую работу, но **блокирует
|
- **Вопрос** — решение человека. Не останавливает начатую работу, но **блокирует
|
||||||
взятие** задачи в спринт. Живёт внутри файла задачи разделом «Вопросы» и тегом
|
взятие** задачи в спринт. Живёт внутри файла задачи разделом «Вопросы» и тегом
|
||||||
@@ -116,7 +118,9 @@ description: "Ритуал между спринтами и ведение са
|
|||||||
Это зависимость, а не список.
|
Это зависимость, а не список.
|
||||||
|
|
||||||
1. **Разбор вопросов.**
|
1. **Разбор вопросов.**
|
||||||
2. **Разбор прошедшего спринта — про процесс, а не про задачи.**
|
2. **Разбор прошедшего спринта — про процесс, а не про задачи.** Здесь же оба
|
||||||
|
судьи документов канона на весь канон разом, раз в спринт: `doc-consistency`
|
||||||
|
(документы между собой) и `doc-code-drift` (документы против кода).
|
||||||
3. **Переоценка задач** порциями.
|
3. **Переоценка задач** порциями.
|
||||||
4. **Выбор цели и набор спринта.** Цель называет человек, набор собирает агент и
|
4. **Выбор цели и набор спринта.** Цель называет человек, набор собирает агент и
|
||||||
показывает **до старта работ**.
|
показывает **до старта работ**.
|
||||||
@@ -143,6 +147,26 @@ flowchart TD
|
|||||||
Схема — **сводка**: процедура каждого шага в
|
Схема — **сводка**: процедура каждого шага в
|
||||||
[references/cadence.md](references/cadence.md), и при расхождении прав текст.
|
[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).
|
интерактива и доклад — [references/cadence.md](references/cadence.md).
|
||||||
|
|
||||||
@@ -233,8 +257,8 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
|||||||
|
|
||||||
## Слоты проекта
|
## Слоты проекта
|
||||||
|
|
||||||
Сессия не знает ни языка, ни сборки, ни CI. Часть проектного отвечает
|
Сессия не знает ни языка, ни сборки, ни CI. На часть проектного отвечает своей
|
||||||
[канон](../canon/references/canon.md) структурой: разбор процесса (шаг 2) живёт
|
структурой [канон](../canon/references/canon.md): разбор процесса (шаг 2) живёт
|
||||||
в `docs/review.md`, оракулы и «чем краснеет безусловно» — в семантике гейта в
|
в `docs/review.md`, оракулы и «чем краснеет безусловно» — в семантике гейта в
|
||||||
`CLAUDE.md`. Остальное проект **дописывает в `CLAUDE.md`**:
|
`CLAUDE.md`. Остальное проект **дописывает в `CLAUDE.md`**:
|
||||||
|
|
||||||
@@ -249,8 +273,8 @@ python3 $tk reopen <слаг> --dir D --reason … # приёмка не со
|
|||||||
это **ориентир, а не закон**.
|
это **ориентир, а не закон**.
|
||||||
|
|
||||||
Слота «куда копируются критерии приёмки» здесь нет намеренно: на него отвечает
|
Слота «куда копируются критерии приёмки» здесь нет намеренно: на него отвечает
|
||||||
**пайплайн проекта**, перенося их в описание изменения при его заведении. Проект
|
**пайплайн проекта** — он переносит критерии в описание изменения, когда его
|
||||||
без пайплайна называет своё место сам, в слоте 1.
|
заводит. Проект без пайплайна называет своё место сам, в слоте 1.
|
||||||
|
|
||||||
Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост
|
Числа проекта (сколько задач в спринте, сколько времени на задачу, каков прирост
|
||||||
беклога) — предмет шага 2, а не константы этого скилла.
|
беклога) — предмет шага 2, а не константы этого скилла.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
## Шаг 1. Разбор вопросов
|
## Шаг 1. Разбор вопросов
|
||||||
|
|
||||||
`tasks.py list --questions` — всё, что накопилось. Вопрос это решение человека,
|
`tasks.py list --questions` — всё, что накопилось. Вопрос это решение человека,
|
||||||
и разбирается он **пачкой**, а не по одному в момент возникновения: по одному —
|
и разбирается он **пачкой**, а не по одному, как только возник: по одному —
|
||||||
это дёрганье, пачкой — это сессия.
|
это дёрганье, пачкой — это сессия.
|
||||||
|
|
||||||
Порядок по каждому вопросу:
|
Порядок по каждому вопросу:
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
2. **Сформулируй развилку** с вариантами и последствием каждого, рекомендация —
|
2. **Сформулируй развилку** с вариантами и последствием каждого, рекомендация —
|
||||||
первым вариантом.
|
первым вариантом.
|
||||||
3. **Вынеси пачкой** через `AskUserQuestion`, не больше трёх за раз.
|
3. **Вынеси пачкой** через `AskUserQuestion`, не больше трёх за раз.
|
||||||
4. **Ответ записывается в тело задачи, раздел «Вопросы» опустошается**, тег
|
4. **Запиши ответ в тело задачи, опустоши раздел «Вопросы»**, сними тег
|
||||||
снимается `edit <slug> --rm-tag question`, **«зачем» переписывается**: «Решено:
|
(`edit <slug> --rm-tag question`), **перепиши «зачем»**: «Решено:
|
||||||
…» на вопрос «почему это лежит в беклоге» уже не отвечает. Опустошение
|
…» на вопрос «почему это лежит в беклоге» уже не отвечает. Опустошение
|
||||||
раздела — не уборка, а условие взятия: правило и причина в скилле `tasks`,
|
раздела — не уборка, а условие взятия: правило и причина в скилле `tasks`,
|
||||||
[references/task-format.md](../../tasks/references/task-format.md).
|
[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. Переоценка задач
|
## Шаг 3. Переоценка задач
|
||||||
|
|
||||||
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
|
Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное
|
||||||
@@ -105,29 +134,41 @@
|
|||||||
4. **Пере-кластеризуй по общей причине.** Несколько задач, оказавшихся симптомами
|
4. **Пере-кластеризуй по общей причине.** Несколько задач, оказавшихся симптомами
|
||||||
одного дефекта, сливаются в одну — это находка, которую интейк дать не мог.
|
одного дефекта, сливаются в одну — это находка, которую интейк дать не мог.
|
||||||
5. **Гигиена полей** — протухшее «зачем», вопрос в прозе, снятый ответ, свойство
|
5. **Гигиена полей** — протухшее «зачем», вопрос в прозе, снятый ответ, свойство
|
||||||
репозитория в рамках, предписание процесса в теле, род работы, разошедшийся с
|
репозитория в рамках, предписание процесса в теле, тип, разошедшийся с
|
||||||
задачей, границы вместо реализации в разделе «Затрагивает». Список и правила —
|
задачей, границы вместо реализации в разделе «Затрагивает». Список и правила —
|
||||||
в скилле `tasks`. **Переоценка — то самое место, где беклог добирает род
|
в скилле `tasks`. **Переоценка — то самое место, где беклог добирает тип и
|
||||||
работы и границы:** требовать их на входе значило бы выгонять в заметки то,
|
разделы его схемы:** требовать их на входе значило бы выгонять в заметки то,
|
||||||
что должно лежать задачей, а к взятию в спринт они уже обязательны.
|
что должно лежать задачей, а к взятию в спринт они уже обязательны. Блок
|
||||||
|
здоровья `check` печатает, сколько записей готово к взятию, — по этому числу
|
||||||
|
и видно, добрала переоценка или нет.
|
||||||
|
|
||||||
Затем — то, что решает пользователь:
|
Затем — то, что решает пользователь:
|
||||||
|
|
||||||
6. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
|
6. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал
|
||||||
сценарий, обошли иначе. Здесь и звучит вопрос о выкидывании.
|
сценарий, обошли иначе. Здесь и звучит вопрос, выкидывать ли.
|
||||||
7. **Та ли цель — и нужна ли она вообще.** Приоритетов нет, и «повысить» нечего
|
7. **Та ли цель — и нужна ли она вообще.** Приоритетов нет, и «повысить» нечего
|
||||||
— вместо повышения **смена цели** (`edit <slug> --goal <другой>`) или
|
— вместо повышения задача **меняет цель** (`edit <slug> --goal <другой>`) или
|
||||||
включение в ближайший набор. `feature`, которой не находится цель, — кандидат
|
входит в ближайший набор. `feature`, которой не находится цель, — кандидат
|
||||||
на выход: новая возможность вне цели это возможность, которой никто не
|
на выход: новая возможность вне цели это возможность, которой никто не
|
||||||
заказывал. Операционной задаче (`fix`, `chore`, `research`) цель не нужна, и
|
заказывал. Операционной задаче (`fix`, `chore`, `research`) цель не нужна, и
|
||||||
выдумывать её здесь не надо.
|
выдумывать её здесь не надо.
|
||||||
|
|
||||||
|
**Отменяется и сама цель** — когда замысел оказался неверен, а не когда
|
||||||
|
задача выбрала не ту. Тогда порция расширяется до всех задач этой цели: каждую
|
||||||
|
либо закрыть своей причиной, либо перевесить на другую цель, и только потом
|
||||||
|
закрыть цель. Порядок и почему он такой —
|
||||||
|
[task-goal.md](../../tasks/references/task-goal.md#отменённая-цель--сперва-задачи-потом-цель).
|
||||||
|
Здесь этому и место: отмена цели это разбор её задач, а разбор задач — этот
|
||||||
|
шаг.
|
||||||
8. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit
|
8. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit
|
||||||
<slug> --type idea`, дальше штурм. Разрослась → это несколько задач под той
|
<slug> --type research` и опустошённый раздел «Вопрос», то есть сырьё; дальше
|
||||||
же целью, дальше декомпозиция.
|
штурм. Разрослась → это несколько задач под той же целью, дальше
|
||||||
9. **Переоценка по измеренному.** Спринт производит числа — сколько на самом
|
декомпозиция.
|
||||||
деле стоит такая работа, что оказалось дороже ожидания. Эти числа меняют цену
|
9. **Переоценка по пройденному.** Прошедший спринт показывает, чего на самом
|
||||||
**других** задач, и именно здесь это применяется: задача, чья цена выросла
|
деле стоит такая работа. Это меняет цену **других** задач, и именно здесь
|
||||||
втрое, а польза осталась прежней, — кандидат на выход.
|
применяется: задача, оказавшаяся заметно дороже, чем думалось, при прежней
|
||||||
|
пользе — кандидат на выход. Судит человек по тому, что помнит о прошедшем
|
||||||
|
спринте; замеров процесс не ведёт и оценок не хранит.
|
||||||
|
|
||||||
### Храповик на залежавшихся
|
### Храповик на залежавшихся
|
||||||
|
|
||||||
@@ -167,7 +208,7 @@
|
|||||||
> - Взять в ближайший набор — без бэкапа ретеншн опасен
|
> - Взять в ближайший набор — без бэкапа ретеншн опасен
|
||||||
> - Выкинуть
|
> - Выкинуть
|
||||||
> 3. `guessit-sputnik` — вынести распознавание в сервис-спутник
|
> 3. `guessit-sputnik` — вынести распознавание в сервис-спутник
|
||||||
> - Понизить до `[idea]` *(рекомендую)* — не проходит тест «готова к взятию»
|
> - Понизить до сырья (`--type research`) *(рекомендую)* — не проходит тест «готова к взятию»
|
||||||
> - Оставить задачей
|
> - Оставить задачей
|
||||||
|
|
||||||
Каждый вариант несёт причину — ту самую, что уедет в `--reason`. Ответы применяй
|
Каждый вариант несёт причину — ту самую, что уедет в `--reason`. Ответы применяй
|
||||||
@@ -184,15 +225,16 @@
|
|||||||
2. **Цель называет человек.** Это продуктовое решение, а не механика: агент
|
2. **Цель называет человек.** Это продуктовое решение, а не механика: агент
|
||||||
предлагает и объясняет, но не выбирает.
|
предлагает и объясняет, но не выбирает.
|
||||||
3. **Набор собирает агент** — `sprint start --goal <слаг>`, затем `sprint take
|
3. **Набор собирает агент** — `sprint start --goal <слаг>`, затем `sprint take
|
||||||
…`. Скрипт не даст взять цель, идею, задачу с чужой целью, с открытым
|
…`. Скрипт не даст взять цель, задачу с чужой целью, с открытым вопросом, без
|
||||||
вопросом, без критериев приёмки, без рода работы или без раздела
|
типа и **без разделов, которых требует её тип** (у `fix` это в том числе
|
||||||
«Затрагивает». Задача без цели вовсе (`fix`, `chore`, `research`) берётся
|
`Воспроизведение`, у `research` — `Вопрос` и `Куда ляжет ответ`, и сырьё
|
||||||
свободно — операционная работа входит в набор помимо его цели.
|
поэтому не берётся вовсе). Задача без цели (`fix`, `chore`, `research`)
|
||||||
|
берётся свободно — операционная работа входит в набор помимо его цели.
|
||||||
4. **Набор показывается человеку до старта работ.** Показ — это и есть момент
|
4. **Набор показывается человеку до старта работ.** Показ — это и есть момент
|
||||||
заморозки: после него набор не двигается. **В показе называется состав по
|
заморозки: после него набор не двигается. **В показе называется состав по
|
||||||
роду работы** — три `fix` и ни одной `feature` под целью развития это
|
типам** — три `fix` и ни одной `feature` под целью развития это разговор про
|
||||||
разговор про цель, а не про набор, и увидеть его надо до заморозки, а не в
|
цель, а не про набор, и увидеть его надо до заморозки, а не в докладе по
|
||||||
докладе по итогам.
|
итогам.
|
||||||
|
|
||||||
Здесь же последний дешёвый момент заметить **разнородную задачу**: раздел
|
Здесь же последний дешёвый момент заметить **разнородную задачу**: раздел
|
||||||
«Затрагивает» показывает границы до того, как заведено предложение об
|
«Затрагивает» показывает границы до того, как заведено предложение об
|
||||||
@@ -200,10 +242,9 @@
|
|||||||
перечня, — кандидат на разрез (шов — в `tasks`, `references/split.md`).
|
перечня, — кандидат на разрез (шов — в `tasks`, `references/split.md`).
|
||||||
Замеченная здесь, она стоит одного `edit`; замеченная на ревью — выброшенного
|
Замеченная здесь, она стоит одного `edit`; замеченная на ревью — выброшенного
|
||||||
предложения.
|
предложения.
|
||||||
5. Задача, которой для взятия не хватает только критериев приёмки, границ или
|
5. Задача, которой для взятия не хватает только разделов её типа, дописывается
|
||||||
рода, дописывается здесь же — 2–5 утверждений с оракулами, перечень
|
здесь же — критерии с оракулами, перечень границ, шаги воспроизведения. Но
|
||||||
затрагиваемых границ, `--kind`. Но если для этого нужен ответ человека, это
|
если для этого нужен ответ человека, это вопрос, и задача в набор не идёт.
|
||||||
вопрос, и задача в набор не идёт.
|
|
||||||
|
|
||||||
**Размер — ориентир, а не закон:** 5–8 задач. Можно взять больше, можно меньше —
|
**Размер — ориентир, а не закон:** 5–8 задач. Можно взять больше, можно меньше —
|
||||||
набор под цель важнее круглого числа; одна крупная задача спринтом тоже бывает.
|
набор под цель важнее круглого числа; одна крупная задача спринтом тоже бывает.
|
||||||
@@ -213,9 +254,11 @@
|
|||||||
- Что просмотрено: N из M, сколько порций, по какому признаку отобраны.
|
- Что просмотрено: N из M, сколько порций, по какому признаку отобраны.
|
||||||
- Вопросы: разобрано N, из них отвечено без человека N, снято тегов N.
|
- Вопросы: разобрано N, из них отвечено без человека N, снято тегов N.
|
||||||
- Разбор процесса: что записано и куда.
|
- Разбор процесса: что записано и куда.
|
||||||
|
- Сверка документов с кодом: звался ли `doc-code-drift`, что проверено из
|
||||||
|
названного, что разошлось.
|
||||||
- Изменения списком: удалено как реализованное (со ссылками), ушло без
|
- Изменения списком: удалено как реализованное (со ссылками), ушло без
|
||||||
реализации (с причинами), понижено до идей, слито, сменило цель.
|
реализации (с причинами), понижено до сырья, слито, сменило тип или цель.
|
||||||
- Новый спринт: цель, набор со слагами, дата, состав по роду работы.
|
- Новый спринт: цель, набор со слагами, дата, состав по типам.
|
||||||
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
|
- **Границы покрытия**: сколько задач не трогали и какие именно секции, теги или
|
||||||
цели остались — иначе доклад читается как «беклог разобран».
|
цели остались — иначе доклад читается как «беклог разобран».
|
||||||
- `tasks.py check` после правок — результат строкой.
|
- `tasks.py check` после правок — результат строкой.
|
||||||
|
|||||||
@@ -81,6 +81,16 @@ flowchart TD
|
|||||||
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
|
делается после ответа человека. Спринт не «ждёт»: ждать может человек, а
|
||||||
замороженный набор, который нельзя двигать, только мешает.
|
замороженный набор, который нельзя двигать, только мешает.
|
||||||
|
|
||||||
|
**Протухший набор — второй законный повод роспуска.** Работа стояла, и человек
|
||||||
|
вернулся к спринту, состав которого уже не держит в голове. Тем же роспуском:
|
||||||
|
`sprint close --dissolve --reason "работа стояла с <когда>"`, недоделанное в
|
||||||
|
беклог, новый набор — после переоценки, а не поверх старого.
|
||||||
|
|
||||||
|
Порога в неделях нет и не будет: счётчик простоя пришлось бы вести руками, а
|
||||||
|
решает всё равно человек. Признак — не срок, а **что набор перестал быть твоим**:
|
||||||
|
взялся перечитывать, зачем эти задачи вместе, — он протух. Заморозка тут не
|
||||||
|
мешает, она запрещает *двигать* набор, а не распустить его целиком.
|
||||||
|
|
||||||
## Определение готовности
|
## Определение готовности
|
||||||
|
|
||||||
Задача засчитывается сделанной, когда верно **всё**:
|
Задача засчитывается сделанной, когда верно **всё**:
|
||||||
@@ -95,10 +105,10 @@ flowchart TD
|
|||||||
отвечает «сделано по правилам», критерии — «сделано то, что заказывали».
|
отвечает «сделано по правилам», критерии — «сделано то, что заказывали».
|
||||||
3. **Находки по ходу отданы списком** — исполнитель обязан их **назвать**
|
3. **Находки по ходу отданы списком** — исполнитель обязан их **назвать**
|
||||||
(каждую, с пометкой «заведена / не заведена: причина»), но **не обязан
|
(каждую, с пометкой «заведена / не заведена: причина»), но **не обязан
|
||||||
заводить**: заведение интерактивно, оно требует дедупликации против беклога и
|
заводить**: заведение интерактивно — оно требует дедупликации против беклога
|
||||||
кладбища и решений человека. Обязанность **завести урожай** — на закрытии
|
и кладбища, а ещё решений человека. Обязанность **завести урожай** — на
|
||||||
спринта, ниже. Так автономный исполнитель не оказывается одновременно обязан
|
закрытии спринта, ниже. Иначе автономный исполнитель оказался бы разом и
|
||||||
завести задачи и не вправе это сделать в одиночку.
|
обязан завести задачи, и не вправе сделать это в одиночку.
|
||||||
|
|
||||||
### Кто и когда закрывает
|
### Кто и когда закрывает
|
||||||
|
|
||||||
@@ -122,8 +132,8 @@ flowchart TD
|
|||||||
|
|
||||||
**Само закрытие тоже коммитится, отдельным коммитом.** Удаление файла задачи и
|
**Само закрытие тоже коммитится, отдельным коммитом.** Удаление файла задачи и
|
||||||
правка индекса — правки в рабочем дереве; пока они не в истории, `SPRINT.md`
|
правка индекса — правки в рабочем дереве; пока они не в истории, `SPRINT.md`
|
||||||
ничего не показывает, а `reopen` восстанавливает текст из `HEAD` в узком окне,
|
ничего не показывает, а `reopen` восстанавливает текст из `HEAD` в узком окне:
|
||||||
которое закончится первым посторонним коммитом. Сообщение про учёт, а не про
|
его закроет первый посторонний коммит. Сообщение про учёт, а не про
|
||||||
работу: `закрыта задача <slug>`.
|
работу: `закрыта задача <slug>`.
|
||||||
|
|
||||||
**Дорога назад существует и обязана быть названа.** Человек на сессии сверил
|
**Дорога назад существует и обязана быть названа.** Человек на сессии сверил
|
||||||
@@ -141,10 +151,10 @@ flowchart TD
|
|||||||
1. **Критерии переживают файл задачи.** Файл удаляется при закрытии, поэтому
|
1. **Критерии переживают файл задачи.** Файл удаляется при закрытии, поэтому
|
||||||
критерии копируются туда, где их увидит приёмщик. Куда именно — **отвечает
|
критерии копируются туда, где их увидит приёмщик. Куда именно — **отвечает
|
||||||
пайплайн проекта, а не слот в `CLAUDE.md`**: он переносит их в `tasks.md`
|
пайплайн проекта, а не слот в `CLAUDE.md`**: он переносит их в `tasks.md`
|
||||||
изменения на шаге заведения change. Проект без пайплайна называет своё место
|
изменения, когда заводит change. Проект без пайплайна называет своё место
|
||||||
сам.
|
сам.
|
||||||
2. **Принимает человек на сессии, а не отдельный агент.** Декорреляция
|
2. **Принимает человек на сессии, а не отдельный агент.** Исполнитель и приёмщик
|
||||||
исполнителя и приёмщика в момент закрытия **снята** (решение о снятии и его
|
в момент закрытия **не разведены** (решение о снятии и его
|
||||||
цена — в `SKILL.md`, «Стимулы»). Опоры остались три: **сохранённый независимый
|
цена — в `SKILL.md`, «Стимулы»). Опоры остались три: **сохранённый независимый
|
||||||
отчёт ревью** (при конвейере `av-dev-pipeline` — отчёт триажа в
|
отчёт ревью** (при конвейере `av-dev-pipeline` — отчёт триажа в
|
||||||
`openspec/changes/archive/<id>/review/`, до архивации — `changes/<id>/review/`),
|
`openspec/changes/archive/<id>/review/`, до архивации — `changes/<id>/review/`),
|
||||||
|
|||||||
+197
-137
@@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
name: tasks
|
name: tasks
|
||||||
description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). Заведение задачи, идеи или цели из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, мозговой штурм идеи, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Ритуал между спринтами — скилл session. Не реализует задачи — этим занимается пайплайн проекта.
|
description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). У каждой записи есть тип (goal, feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. Заведение записи из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, штурм сырья, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Ритуал между спринтами — скилл session. Не реализует задачи — этим занимается пайплайн проекта.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Задачи
|
# Задачи
|
||||||
|
|
||||||
Задачи — каталог markdown-файлов. Одна запись = один файл `items/<slug>.md` плюс
|
Задачи — каталог markdown-файлов. Одна запись = один файл `items/<slug>.md` плюс
|
||||||
строка **ровно в одном** индексе. Скилл владеет **форматом и содержимым**:
|
строка **ровно в одном** индексе. Скилл владеет **форматом и содержимым**:
|
||||||
заводит, редактирует, закрывает, разбирает находки ревью, дробит, штурмует идеи.
|
заводит, редактирует, закрывает, разбирает находки ревью, дробит, штурмует сырьё.
|
||||||
|
|
||||||
Чем он **не** владеет: ритуалом между спринтами (разбор вопросов → разбор
|
Чем он **не** владеет: ритуалом между спринтами (разбор вопросов → разбор
|
||||||
прошедшего спринта → переоценка → выбор цели и набор) — это скилл `session`; и
|
прошедшего спринта → переоценка → выбор цели и набор) — это скилл `session`; и
|
||||||
выполнением задачи — это пайплайн проекта.
|
выполнением задачи — это пайплайн проекта.
|
||||||
|
|
||||||
## Пять правил, из которых всё следует
|
## Шесть правил, из которых всё следует
|
||||||
|
|
||||||
Ситуация не покрыта инструкцией — решай по ним.
|
Ситуация не покрыта инструкцией — решай по ним.
|
||||||
|
|
||||||
@@ -43,10 +43,20 @@ description: Ведение задач и целей как каталога mar
|
|||||||
4. **Порядка нет, есть цель — но цель есть не у всякой задачи.** Приоритетов,
|
4. **Порядка нет, есть цель — но цель есть не у всякой задачи.** Приоритетов,
|
||||||
«повысить» и «встать раньше» нет: «что делать дальше» отвечает набор спринта,
|
«повысить» и «встать раньше» нет: «что делать дальше» отвечает набор спринта,
|
||||||
а между спринтами порядок не нужен никому. Цель обязательна там, где она и
|
а между спринтами порядок не нужен никому. Цель обязательна там, где она и
|
||||||
есть содержание работы, — у **новой возможности** (`kind:feature`). Починка,
|
есть содержание работы, — у **новой возможности** (`feature`). Починка,
|
||||||
техдолг и разведка служат работоспособности, а не направлению, и живут без
|
техдолг и разведка служат работоспособности, а не направлению, и живут без
|
||||||
цели законно; в набор спринта они входят помимо его цели. Придуманная им цель
|
цели законно; в набор спринта они входят помимо его цели. Придуманная им цель
|
||||||
— то же враньё, от которого спасает род работы.
|
— то же враньё, от которого спасает тип.
|
||||||
|
|
||||||
|
Единственный порядок, который в беклоге всё-таки есть, **производен от типа**,
|
||||||
|
а не назначен человеком: **сырьё** (`research` без раздела «Вопрос») стоит в
|
||||||
|
конце своей категории. Его не берут, и между берущимся оно каждый раз требует
|
||||||
|
открыть файл, чтобы это понять. Раз порядок выводится, его проверяет машина —
|
||||||
|
и приоритетом он не становится.
|
||||||
|
5. **Тип решает, что с записью можно делать.** Тип — единственная ось и первое
|
||||||
|
поле меты: от него зависит, какие разделы обязательны в теле, нужна ли цель,
|
||||||
|
берётся ли запись в спринт и в каком индексе живёт её строка. Словарь закрыт;
|
||||||
|
ни один тип не подошёл — значит, в записи их два, и её надо разделить.
|
||||||
|
|
||||||
## Раскладка
|
## Раскладка
|
||||||
|
|
||||||
@@ -67,34 +77,44 @@ docs/tasks/
|
|||||||
подо что берут.** Цель в спринт взять нельзя, поэтому в списке берущихся ей не
|
подо что берут.** Цель в спринт взять нельзя, поэтому в списке берущихся ей не
|
||||||
место.
|
место.
|
||||||
|
|
||||||
**Четыре секции роадмапа, и первая отвечает на половину вопроса:**
|
**Четыре секции роадмапа, и последняя отвечает на половину вопроса:**
|
||||||
|
|
||||||
| Секция | Англ. | Что в ней |
|
| Секция | Англ. | Что в ней |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `Готово` | `Done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках |
|
|
||||||
| `Запланировано` | `Planned` | очередь значима и обосновывается прозой рядом |
|
| `Запланировано` | `Planned` | очередь значима и обосновывается прозой рядом |
|
||||||
| `Направления` | `Directions` | очереди нет, тянутся долго |
|
| `Направления` | `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` оно уже значит боевое окружение приложения, и одно слово в двух
|
`architecture.md` оно уже значит боевое окружение приложения, и одно слово в двух
|
||||||
смыслах развело бы документы канона.
|
смыслах развело бы документы канона. А `Разработка`, стоявшая тут раньше,
|
||||||
|
называла слишком много: роадмап **весь** про разработку, и секция с таким именем
|
||||||
|
не отличалась от остальных ничем.
|
||||||
|
|
||||||
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (спринт не может
|
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (спринт не может
|
||||||
продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и
|
продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и
|
||||||
@@ -135,10 +155,10 @@ stateDiagram-v2
|
|||||||
state "записи нет — реализована" as D
|
state "записи нет — реализована" as D
|
||||||
state "ROADMAP.md, «умеет» — цель достигнута" as A
|
state "ROADMAP.md, «умеет» — цель достигнута" as A
|
||||||
|
|
||||||
[*] --> B: add
|
[*] --> B: add --type feature|fix|chore|research
|
||||||
[*] --> P: add --type goal
|
[*] --> P: add --type goal
|
||||||
B --> P: edit --type goal --section
|
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
|
B --> S: sprint take
|
||||||
S --> B: sprint drop --reason
|
S --> B: sprint drop --reason
|
||||||
S --> D: close --implemented
|
S --> D: close --implemented
|
||||||
@@ -161,7 +181,7 @@ stateDiagram-v2
|
|||||||
|
|
||||||
## Цели
|
## Цели
|
||||||
|
|
||||||
**Цель — возможность приложения.** Такой же файл в `items/`, тип `[goal]`,
|
**Цель — возможность приложения.** Такой же файл в `items/`, тип `goal` (🎯),
|
||||||
перечисленный в `ROADMAP.md`. Формулируется ответом на вопрос **«что приложение
|
перечисленный в `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`
|
проектов. Встретился в чужом беклоге — это цель либо набор задач, и `check`
|
||||||
назовёт его неизвестным типом.
|
назовёт его неизвестным типом.
|
||||||
|
|
||||||
## Род работы
|
## Тип записи
|
||||||
|
|
||||||
**Тип записи и род работы — две оси, и путать их нельзя.** Тип отвечает «что это
|
**Тип — единственная ось, и он решает, что с записью можно делать.** Дом типа —
|
||||||
за запись» (цель, идея, задача), род — «какого рода работа»: `feature`, `fix`,
|
**поле меты `Тип` первой строкой**; эмодзи в заголовке H1 от него производна, её
|
||||||
`chore`, `research`. Одним значением на оба вопроса не ответить: идея бывает
|
ставит `add` и чинит `check --fix`.
|
||||||
*про* функцию, а цель функцией *и является*.
|
|
||||||
|
|
||||||
- **`feature`** — снаружи появляется или меняется то, чего раньше не было.
|
| Тип | Обязательные разделы | Цель | В спринт | Устав |
|
||||||
- **`fix`** — поведение расходится с заявленным, и расхождение воспроизводится.
|
| --- | --- | --- | --- | --- |
|
||||||
Не воспроизводится — это `research`, а не `fix`.
|
| 🎯 `goal` | `Завершение` | — | нет | [task-goal.md](references/task-goal.md) |
|
||||||
- **`chore`** — обслуживание: зависимости, сборка, перенос, чистка. Наблюдаемое
|
| ✨ `feature` | `Затрагивает`, `Критерии приёмки` | **обязательна** | да | [task-feature.md](references/task-feature.md) |
|
||||||
поведение не меняется, и в этом всё дело: **у `chore` тест готовности слабее
|
| 🐞 `fix` | `Воспроизведение`, `Затрагивает`, `Критерии приёмки` | необязательна | да | [task-fix.md](references/task-fix.md) |
|
||||||
честно**, а не молча. «Что станет наблюдаемо иначе» здесь отвечается
|
| 🧹 `chore` | `Затрагивает`, `Критерии приёмки` | нет | да | [task-chore.md](references/task-chore.md) |
|
||||||
разработчику («перестанет собираться два раза», «уедет последний вызов
|
| 🔬 `research` | `Вопрос`, `Куда ляжет ответ` | нет | да | [task-research.md](references/task-research.md) |
|
||||||
устаревшего API»), а не пользователю. Пока рода не было, такие задачи либо не
|
|
||||||
заводились, либо формулировались как выдуманная польза.
|
|
||||||
- **`research`** — исход работы знание, а не изменение системы: ответ на вопрос,
|
|
||||||
замер, разведка. Приёмка — записанный ответ (`docs/research/`, ADR, тело
|
|
||||||
задачи), а не изменённый код.
|
|
||||||
|
|
||||||
Дом рода — **тег `kind:<род>`**, а не префикс заголовка и не поле меты: теги
|
Сверх обязательных у любой задачи допустимы `Рамки` и `Вопросы`. Раздел не из
|
||||||
здесь единственный механизм разметки, и `list --kind fix` работает даром. Цена
|
схемы своего типа — **замечание, а не ошибка**: свой раздел законная вольность
|
||||||
известна: в строку индекса род не попадает (индексы производны), и «в наборе одни
|
проекта, но `Воспроизведение` у `chore` почти всегда значит, что тип проставлен
|
||||||
починки» видно командой, а не глазами по `SPRINT.md`.
|
не тот, и сказать об этом стоит, не запрещая.
|
||||||
|
|
||||||
|
**Осей было две, и ортогональность у них была фальшивой.** Тип записи
|
||||||
|
(`goal`/`idea`/`task`) и род работы (`kind:<род>` тегом) давали двенадцать клеток
|
||||||
|
произведения, из которых законны были шесть: у цели род запрещён, у задачи
|
||||||
|
обязателен, у идеи пуст. Плюс алгоритм работы крепится не к `task`, а к `fix` и
|
||||||
|
`research` — то есть к роду. Оси схлопнуты, тег `kind:` упразднён.
|
||||||
|
|
||||||
|
**Тип `idea` упразднён вместе с ними.** Он значил не род работы, а **состояние
|
||||||
|
незаполненности** — «первый, второй или третий вопрос теста готовности не
|
||||||
|
отвечается», — а состояние типом быть не может: оно меняется по мере того, как
|
||||||
|
запись дописывают, а тип меняют командой. Теперь это состояние называется честно:
|
||||||
|
`research` без раздела «Вопрос» — **сырьё**. В спринт не берётся ровно как
|
||||||
|
прежняя идея, лежит в конце своей категории и отбирается `list --raw`.
|
||||||
|
|
||||||
Словарь **закрыт**. Открытый разъедется на синонимах — `bug`, `bugfix`, `fix`,
|
Словарь **закрыт**. Открытый разъедется на синонимах — `bug`, `bugfix`, `fix`,
|
||||||
`defect`, — и отбор по роду перестанет отвечать на свой единственный вопрос. Ни
|
`defect`, — и отбор по типу перестанет отвечать на свой единственный вопрос. Ни
|
||||||
один род не подходит — это сигнал, что в задаче их два и её надо разделить.
|
один тип не подходит — это сигнал, что в задаче их два и её надо разделить.
|
||||||
|
|
||||||
**Род обязателен у задачи, у цели запрещён, у идеи необязателен** — идея получает
|
**Требуется тип там, где по нему принимают решение:** `sprint take` без типа
|
||||||
его, когда становится задачей. Требуется он там, где по нему принимают решение:
|
откажет, потому что не знает, каких разделов требовать. `check` о пропаже только
|
||||||
`sprint take` без рода откажет. `check` о пропаже только **напоминает** — беклог,
|
**напоминает** — беклог, заведённый до появления типа, законен, и переоформлять
|
||||||
заведённый до появления рода, законен, и переоформлять его «заодно» здесь не
|
его «заодно» здесь не просят.
|
||||||
просят.
|
|
||||||
|
|
||||||
**Род решает и то, обязательна ли цель.** `feature` без цели не бывает: новая
|
**Тип не выбирает профиль ревью и вообще ничего не предписывает пайплайну.**
|
||||||
возможность и есть содержание цели, и если подходящей нет — либо она заводится,
|
Профиль выбирается по факту изменения, а не по типу задачи: `chore` бывает
|
||||||
либо это не `feature`. `fix`, `chore` и `research` живут без цели законно, и
|
|
||||||
`check` о них молчит: они служат работоспособности, а не направлению. Это
|
|
||||||
единственный случай, когда род что-то определяет за пределами отбора, — и
|
|
||||||
определяет он учёт, а не процесс проверки.
|
|
||||||
|
|
||||||
**Род не выбирает профиль ревью и вообще ничего не предписывает пайплайну.**
|
|
||||||
Профиль выбирается по факту изменения, а не по роду задачи: `chore` бывает
|
|
||||||
миграцией схемы, `fix` — правкой публичного контракта. Правило «предписание
|
миграцией схемы, `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 # согласованность индексов + здоровье
|
||||||
python3 $tk check --dir D --fix # + починить дрейф (секция, заголовок, дубли, «зачем», форма меты)
|
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 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|idea] [--section S] [--goal G] [--kind K] [--why «зачем»] [--tag a,b]
|
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] [--kind K] [--add-tag a,b] [--rm-tag c]
|
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 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 --reason R # в REJECTED.md + удалить (ушла без реализации)
|
||||||
python3 $tk close S --dir D --implemented # просто удалить (реализована и закоммичена)
|
python3 $tk close S --dir D --implemented # просто удалить (реализована и закоммичена)
|
||||||
python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась
|
python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась
|
||||||
python3 $tk sprint start --goal S --dir D | take S… | drop S… --reason R | close [--dissolve --reason R]
|
python3 $tk sprint start --goal S --dir D | take S… | drop S… --reason R | close [--dissolve --reason R]
|
||||||
python3 $tk init --dir D [--sections …] [--roadmap-sections …] [--items …] [--backlog …] …
|
python3 $tk init --dir D [--sections …] [--items …] [--backlog …] …
|
||||||
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md
|
python3 $tk adopt scan --from … | apply --plan … # разовая адаптация, references/adopt.md
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -354,20 +387,22 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
|
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
|
||||||
нет» — нерабочая, и одинаковая реакция на них была бы неверна в обоих случаях.
|
нет» — нерабочая, и одинаковая реакция на них была бы неверна в обоих случаях.
|
||||||
|
|
||||||
Тип — английское ключевое слово `goal` / `idea` / `task` (как и прочие токены
|
Тип — английское ключевое слово `goal` / `feature` / `fix` / `chore` /
|
||||||
команд); `task` префикса не несёт, остальные кодируются `[goal]`/`[idea]` в
|
`research` (как и прочие токены команд), у `add` **обязательное**: без него
|
||||||
заголовке. Текст задачи при этом русский.
|
неизвестно, какой шаблон тела класть. Текст задачи при этом русский, а эмодзи в
|
||||||
|
заголовке ставит скрипт.
|
||||||
|
|
||||||
**Мутации правят файл и индексы заодно** — руками строку индекса или мету
|
**Мутации правят файл и индексы заодно** — руками строку индекса или мету
|
||||||
не пиши, зови `add`/`edit`/`move`/`close`/`sprint`. Смена заголовка, «зачем», типа,
|
не пиши, зови `add`/`edit`/`move`/`close`/`sprint`. Смена заголовка, «зачем», типа,
|
||||||
цели, рода работы и **тегов** — это `edit`: он держит H1, мету и индекс в синхроне.
|
цели и **тегов** — это `edit`: он держит H1 (вместе с эмодзи), мету и индекс
|
||||||
Снятие тега — `--rm-tag` (после ответа на вопрос снимается `question`), смена
|
согласованными. Снятие тега — `--rm-tag` (после ответа на вопрос снимается
|
||||||
цели — `--goal`, рода — `--kind`; оба заменяют прежнее значение, а не добавляют
|
`question`), смена цели — `--goal`, типа — `--type`; оба заменяют прежнее
|
||||||
второе.
|
значение, а не добавляют второе.
|
||||||
|
|
||||||
**Переезд между индексами — следствие смены типа, а не отдельная команда.**
|
**Переезд между индексами — следствие смены типа, а не отдельная команда.**
|
||||||
`edit <slug> --type goal --section <часть роадмапа>` переносит строку из
|
`edit <slug> --type goal --section <часть роадмапа>` переносит строку из
|
||||||
`BACKLOG.md` в `ROADMAP.md` (и обратно `--type task --section <секция беклога>`);
|
`BACKLOG.md` в `ROADMAP.md` (и обратно — задачным типом плюс
|
||||||
|
`--section <категория беклога>`);
|
||||||
`move` двигает только внутри одного индекса и пишет причину. `--section` у
|
`move` двигает только внутри одного индекса и пишет причину. `--section` у
|
||||||
`edit` работает **только** при таком переезде — иначе он отсылает к `move`,
|
`edit` работает **только** при таком переезде — иначе он отсылает к `move`,
|
||||||
потому что смена секции без причины и есть тот дрейф, который потом никто не
|
потому что смена секции без причины и есть тот дрейф, который потом никто не
|
||||||
@@ -380,39 +415,54 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
`check` — единственный судья согласованности; что именно он ловит, скажет его
|
`check` — единственный судья согласованности; что именно он ловит, скажет его
|
||||||
вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой
|
вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой
|
||||||
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившееся
|
правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившееся
|
||||||
чини `check --fix` — он детерминированно правит то, где истина однозначна
|
чини `check --fix` — он детерминированно правит то, где истина однозначна (тип в
|
||||||
(секция, заголовок, дубли, «зачем» из индекса в файл, старая форма меты,
|
своё поле, эмодзи заголовка, имя поля места, секция, заголовок, дубли, «зачем» из
|
||||||
пометка `decomposed` у цели с задачами), а неоднозначное (задача сразу в двух
|
индекса в файл, старая форма меты, пометка `decomposed` у цели с задачами, сырьё
|
||||||
индексах, нечего восстанавливать) печатает отдельной пометкой `НЕОДНОЗНАЧНО` —
|
в конец категории), а неоднозначное (задача сразу в двух индексах, нечего
|
||||||
это тебе, и это идёт строкой доклада. **Ссылка на исчезнувший файл в пометку не
|
восстанавливать, **тип, которого неоткуда взять**) печатает отдельной пометкой
|
||||||
попадает:** `--fix` её просто не трогает, и она остаётся `ОШИБКА` обычного
|
`НЕОДНОЗНАЧНО` — это тебе, и это идёт строкой доклада. **Ссылка на исчезнувший
|
||||||
`check` — то есть видна, но в докладе её надо назвать отдельно.
|
файл в пометку не попадает:** `--fix` её просто не трогает, и она остаётся
|
||||||
|
`ОШИБКА` обычного `check` — то есть видна, но в докладе её надо назвать отдельно.
|
||||||
|
|
||||||
`--fix` правит **и файлы** — ровно в двух местах, где источник ровно один и
|
`--fix` правит **и файлы** — там, где источник ровно один и выбирать не из чего:
|
||||||
выбирать не из чего: «зачем», оставшееся только в индексе, переезжает в мету,
|
тип переезжает из прежнего дома (тег `kind:`, префикс `[goal]`/`[idea]`) в поле
|
||||||
и цель, у которой есть задачи, получает тег `decomposed`. Оба случая печатаются
|
меты, заголовок получает эмодзи, поле места — имя по типу, «зачем», оставшееся
|
||||||
поимённо.
|
только в индексе, переезжает в мету, цель с задачами получает `decomposed`.
|
||||||
|
Каждый случай печатается поимённо.
|
||||||
|
|
||||||
|
**Тип, который не выводится ниоткуда, `--fix` не угадывает.** `feature` от
|
||||||
|
`chore` машина не отличает, и подставленное наугад значение врало бы ровно там,
|
||||||
|
где по нему принимают решение. Такие записи идут в `НЕОДНОЗНАЧНО`, и тип им
|
||||||
|
проставляет человек — `edit <слаг> --type …`.
|
||||||
|
|
||||||
**Что механизировано, а что нет.** У задачи, взятой в набор (`sprint take` и
|
**Что механизировано, а что нет.** У задачи, взятой в набор (`sprint take` и
|
||||||
`check` по задачам спринта), проверяются три вещи, и у каждой своя глубина:
|
`check` по задачам спринта), проверяется схема её типа, и у каждой части своя
|
||||||
|
глубина:
|
||||||
|
|
||||||
- **критерии приёмки** — число пунктов жёстко (меньше двух отказ, больше пяти
|
- **тип** — жёстко: назван и из закрытого словаря;
|
||||||
замечание), наличие оракула **эвристикой** по слову «оракул» в пункте;
|
- **критерии приёмки** (`feature`, `fix`, `chore`) — число пунктов жёстко
|
||||||
- **род работы** — жёстко: назван и из закрытого словаря;
|
(меньше двух отказ, больше пяти замечание), наличие оракула **эвристикой** по
|
||||||
- **раздел «Затрагивает»** — только **наличие непустого**. Полнота перечня машине
|
слову «оракул» в пункте;
|
||||||
не видна: границу, которую забыли назвать, она от отсутствующей не отличает.
|
- **прочие разделы схемы** (`Затрагивает`, `Воспроизведение`, `Вопрос`,
|
||||||
|
`Куда ляжет ответ`, `Завершение`) — только **наличие непустого**. Содержимое
|
||||||
|
машине не видно: границу, которую забыли назвать, она от отсутствующей не
|
||||||
|
отличает, а шаги, по которым ничего не воспроизводится, — от годных.
|
||||||
|
|
||||||
Настоящий оракул от слова «оракул» машина тоже не отличает, поэтому эвристика
|
Настоящий оракул от слова «оракул» машина тоже не отличает, поэтому эвристика
|
||||||
даёт только замечание, и в докладе это называется как есть: «проверено число
|
даёт только замечание, и в докладе это называется как есть: «проверено наличие
|
||||||
пунктов и наличие границ, годность оракулов и полнота границ — глазами».
|
разделов своего типа и число критериев, годность оракулов и полнота границ —
|
||||||
|
глазами».
|
||||||
|
|
||||||
Формат файла, меты, слага, индексов и `REJECTED.md` —
|
Формат записи, меты, слага, индексов и `REJECTED.md` —
|
||||||
[references/task-format.md](references/task-format.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. **Фильтр.** Делаем прямо сейчас — не заводим. Не пожалеем о потере — не
|
1. **Фильтр.** Делаем прямо сейчас — не заводим. Не пожалеем о потере — не
|
||||||
заводим. Родилось три кандидата — покажи их и спроси, какие заводить: молча
|
заводим. Родилось три кандидата — покажи их и спроси, какие заводить: молча
|
||||||
@@ -423,30 +473,36 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
ту строку и что изменилось с момента отказа (`add` предупредит и сам, но
|
ту строку и что изменилось с момента отказа (`add` предупредит и сам, но
|
||||||
молча заводить нельзя). Две задачи об одном — самая дорогая находка
|
молча заводить нельзя). Две задачи об одном — самая дорогая находка
|
||||||
переоценки.
|
переоценки.
|
||||||
3. **Тип по тесту готовности** (см. task-format): проходит — задача, не
|
3. **Тип** — `--type` обязателен, и он же первое содержательное решение:
|
||||||
проходит — идея (`--type idea`). Не делается одним заходом — это не эпик, а
|
|
||||||
несколько задач под одной целью: дроби сразу. Возможность приложения, а не
|
- возможность приложения, а не шаг к ней → `goal`;
|
||||||
шаг — цель (`--type goal`).
|
- снаружи появляется то, чего не было → `feature`;
|
||||||
4. **Цель задачи — если род её требует.** У `feature` должен быть
|
- поведение расходится с заявленным и **воспроизводится** → `fix`
|
||||||
`--goal <слаг>`: новая возможность и есть содержание цели. Подходящей нет —
|
(не воспроизводится → `research`);
|
||||||
либо она заводится (`--type goal`), либо перед тобой не `feature`. У `fix`,
|
- обслуживание, наблюдаемое поведение не меняется → `chore`;
|
||||||
`chore` и `research` цели может не быть вовсе, и придумывать её не надо. У
|
- исход — знание, а не изменение системы → `research`.
|
||||||
идеи цель проставляется, когда идея становится задачей.
|
|
||||||
5. **Род работы** — `--kind feature|fix|chore|research` (см. «Род работы»). Не
|
Не подходит ни один — в записи их два, разбирай. Не проходит тест готовности
|
||||||
подходит ни один — задача не одна, разбирай.
|
(см. task-format) — это **сырьё**: `--type research`, раздел «Вопрос» пока
|
||||||
6. `add …`, затем допиши тело редактором: одна фраза, **затрагиваемые границы**,
|
пуст, место в конце категории. Не делается одним заходом — это не эпик, а
|
||||||
критерии приёмки с оракулами, рамки. «Зачем» отвечает «зачем нужна эта
|
несколько задач под одной целью: дроби сразу.
|
||||||
задача» — состояние, остаток, боль, — а не пересказывает первый абзац, и
|
4. **Цель — если тип её требует.** У `feature` должен быть `--goal <слаг>`:
|
||||||
пишется **для человека**: не «канонизация внутри транзакции», а «тело 40 МиБ
|
новая возможность и есть содержание цели. Подходящей нет — либо она
|
||||||
держит блокировку 5 секунд, соседние доставки уходят в отказ».
|
заводится (`--type goal`), либо перед тобой не `feature`. У `fix`, `chore` и
|
||||||
7. `check`.
|
`research` цели может не быть вовсе, и придумывать её не надо.
|
||||||
|
5. `add …`, затем допиши тело редактором **по схеме своего типа** — шаблон её
|
||||||
|
уже разложил, устав типа объясняет каждый раздел. «Зачем» отвечает «зачем
|
||||||
|
нужна эта задача» — состояние, остаток, боль, — а не пересказывает первый
|
||||||
|
абзац, и пишется **для человека**: не «канонизация внутри транзакции», а
|
||||||
|
«тело 40 МиБ держит блокировку 5 секунд, соседние доставки уходят в отказ».
|
||||||
|
6. `check`.
|
||||||
|
|
||||||
### Разобрать находки аудита или ревью
|
### Разобрать находки аудита или ревью
|
||||||
|
|
||||||
Ревью и аудиты — тоже источник задач, но с зеркальной диалогу опасностью: не
|
Ревью и аудиты — тоже источник задач, но с опасностью, зеркальной диалогу: не
|
||||||
пять файлов из одной мысли, а сорок файлов из сорока сырых находок. Защита та
|
пять файлов из одной мысли, а сорок файлов из сорока сырых находок. Защита та
|
||||||
же, что в самом ревью: кластеризация по причине, дедуп против живых и
|
же, что в самом ревью: кластеризация по причине, дедуп против живых и
|
||||||
`REJECTED.md`, находка без свидетельства → идея, а не задача, и карта кластеров
|
`REJECTED.md`, находка без свидетельства → сырьё (`research`), а не задача, и карта кластеров
|
||||||
пользователю до создания файлов. Порядок, отображение серьёзности и привязка к
|
пользователю до создания файлов. Порядок, отображение серьёзности и привязка к
|
||||||
целям — [references/from-review.md](references/from-review.md).
|
целям — [references/from-review.md](references/from-review.md).
|
||||||
|
|
||||||
@@ -460,7 +516,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
Если переводить надо не только задачи, а весь `docs/` — это скилл
|
Если переводить надо не только задачи, а весь `docs/` — это скилл
|
||||||
`av-dev-pm:canon`, и он зовёт этот сценарий сам на своём шаге.
|
`av-dev-pm:canon`, и он зовёт этот сценарий сам на своём шаге.
|
||||||
|
|
||||||
### Декомпозиция и штурм идеи
|
### Декомпозиция и штурм сырья
|
||||||
|
|
||||||
[references/split.md](references/split.md). Обе операции превращают одну запись в
|
[references/split.md](references/split.md). Обе операции превращают одну запись в
|
||||||
несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и
|
несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и
|
||||||
@@ -530,10 +586,14 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
- **предписание процесса в теле** — «делать таким-то профилем ревью», «взять
|
- **предписание процесса в теле** — «делать таким-то профилем ревью», «взять
|
||||||
такой-то агент»: это второй дом для правила выбора и путь понизить требования
|
такой-то агент»: это второй дом для правила выбора и путь понизить требования
|
||||||
решением, принятым до проектирования. Снимается;
|
решением, принятым до проектирования. Снимается;
|
||||||
- **род, разошедшийся с задачей** — задача заводилась починкой, а после разбора
|
- **тип, разошедшийся с задачей** — задача заводилась починкой, а после разбора
|
||||||
оказалось, что поведение никогда и не было заявлено: это `feature`, а не `fix`.
|
оказалось, что поведение никогда и не было заявлено: это `feature`, а не `fix`.
|
||||||
Правится `edit <slug> --kind …`; род, оставшийся от прошлой формулировки, врёт
|
Правится `edit <slug> --type …`; тип, оставшийся от прошлой формулировки, врёт
|
||||||
ровно там, где по нему отбирают;
|
ровно там, где по нему отбирают, **и требует не тех разделов**: у брошенного
|
||||||
|
`fix` останется «Воспроизведение», которого нечем заполнить;
|
||||||
|
- **сырьё, у которого появился вопрос** — разведка обросла формулировкой, но
|
||||||
|
раздел «Вопрос» так и пуст: она числится сырьём и в спринт не берётся.
|
||||||
|
Записывается вопрос, и `check --fix` поднимает строку из конца категории;
|
||||||
- **границы, названные вместо реализации** — «переписать хранилище на новый
|
- **границы, названные вместо реализации** — «переписать хранилище на новый
|
||||||
драйвер» в разделе «Затрагивает» это не граница, а замысел. Границы —
|
драйвер» в разделе «Затрагивает» это не граница, а замысел. Границы —
|
||||||
`таблица points и её миграция`, `эндпоинт POST /ingest`, `формат отпечатка на
|
`таблица points и её миграция`, `эндпоинт POST /ingest`, `формат отпечатка на
|
||||||
@@ -598,7 +658,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
|
|
||||||
- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным
|
- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным
|
||||||
предварительным суждением (**рекомендация — первым вариантом**). Что выкинуть,
|
предварительным суждением (**рекомендация — первым вариантом**). Что выкинуть,
|
||||||
под какую цель отнести, какая рамка идеи верна — решение пользователя. Слаг,
|
под какую цель отнести, какая рамка разведки верна — решение пользователя. Слаг,
|
||||||
формулировка, порядок строк в индексе — механика, делаем сами.
|
формулировка, порядок строк в индексе — механика, делаем сами.
|
||||||
- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа;
|
- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа;
|
||||||
решений больше — веди **несколько итераций** диалога по ≤3, а не один
|
решений больше — веди **несколько итераций** диалога по ≤3, а не один
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
|||||||
- **английские слаги.** Перевести `taj-brejk-pri-ravnoj-polnote` в
|
- **английские слаги.** Перевести `taj-brejk-pri-ravnoj-polnote` в
|
||||||
`tie-break-equal-completeness` может только тот, кто понимает смысл. `scan`
|
`tie-break-equal-completeness` может только тот, кто понимает смысл. `scan`
|
||||||
честно говорит: проверить надо **все** слаги, признаки транслита — эвристика;
|
честно говорит: проверить надо **все** слаги, признаки транслита — эвристика;
|
||||||
- **цели.** Шаги роадмапа — готовые цели из **«порядка»** (очередь и обоснование у
|
- **цели.** Шаги роадмапа — готовые цели в **`Запланировано`** (очередь и
|
||||||
них уже есть); тематические скопления задач — **«темы»** («прочность слияния»,
|
обоснование у них уже есть); тематические скопления задач — цели в
|
||||||
|
**`Направления`** («прочность слияния»,
|
||||||
«журнал и пересборка»). Предлагаешь ты, назначает человек;
|
«журнал и пересборка»). Предлагаешь ты, назначает человек;
|
||||||
- **что вообще не задача.** Обоснование порядка шагов, абзац прозой, заголовок
|
- **что вообще не задача.** Обоснование порядка шагов, абзац прозой, заголовок
|
||||||
раздела — это не пункты беклога, и они уходят в «не разложилось» с причиной.
|
раздела — это не пункты беклога, и они уходят в «не разложилось» с причиной.
|
||||||
@@ -70,7 +71,8 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
|||||||
прохода дадут два несогласованных состояния.
|
прохода дадут два несогласованных состояния.
|
||||||
3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи;
|
3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи;
|
||||||
список `goals` — из шагов роадмапа и из тем. Закрытый шаг целью не
|
список `goals` — из шагов роадмапа и из тем. Закрытый шаг целью не
|
||||||
заводится. Пустой `goal` — законный исход только у идеи.
|
заводится. Пустой `goal` законен у `fix`, `chore` и `research` — они служат
|
||||||
|
работоспособности, а не направлению; у `feature` цель обязательна.
|
||||||
4. **Покажи человеку карту** через `AskUserQuestion`, ≤3 вопроса за итерацию,
|
4. **Покажи человеку карту** через `AskUserQuestion`, ≤3 вопроса за итерацию,
|
||||||
рекомендация первым вариантом. Показывается: сколько записей, предлагаемые
|
рекомендация первым вариантом. Показывается: сколько записей, предлагаемые
|
||||||
цели (порядок и темы) с обоснованием, спорные отнесения, список «не
|
цели (порядок и темы) с обоснованием, спорные отнесения, список «не
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
## Находка агента — не задача
|
## Находка агента — не задача
|
||||||
|
|
||||||
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
|
Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест,
|
||||||
воспроизводимый шаг, положение гайда). Согласие нескольких находок само по себе
|
воспроизводимый шаг, положение руководства). Согласие нескольких находок само по себе
|
||||||
достоверность не повышает: это один источник, высказавшийся несколько раз.
|
достоверность не повышает: это один источник, высказавшийся несколько раз.
|
||||||
|
|
||||||
Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»:
|
Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»:
|
||||||
@@ -23,8 +23,11 @@
|
|||||||
- **Находка со свидетельством**, отложенная к исполнению → **задача**.
|
- **Находка со свидетельством**, отложенная к исполнению → **задача**.
|
||||||
Свидетельство и последствие переносим в тело — это её «почему», то самое, что
|
Свидетельство и последствие переносим в тело — это её «почему», то самое, что
|
||||||
переживает запись.
|
переживает запись.
|
||||||
- **Находка без свидетельства / низкой уверенности** → **идея** (`[idea]`), а не
|
- **Находка без свидетельства / низкой уверенности** → **сырьё**: `research`, у
|
||||||
задача. Её судьба — штурм, где либо найдётся подтверждение, либо она уедет в
|
которого раздел «Вопрос» и есть недостающее свидетельство («при каких условиях
|
||||||
|
это воспроизводится»). Не `fix`: без `Воспроизведения` его в спринт не
|
||||||
|
возьмут, и правильно — чинить нечего, пока непонятно, что ломается. Судьба
|
||||||
|
сырья — штурм, где либо найдётся подтверждение, либо оно уедет в
|
||||||
`REJECTED.md`.
|
`REJECTED.md`.
|
||||||
- **Уже починено по ходу ревью** → **ничего**. Починенное не заводим.
|
- **Уже починено по ходу ревью** → **ничего**. Починенное не заводим.
|
||||||
- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с
|
- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с
|
||||||
@@ -44,13 +47,13 @@
|
|||||||
4. **Разложи по целям — там, где цель нужна.** Большинство находок ревью это
|
4. **Разложи по целям — там, где цель нужна.** Большинство находок ревью это
|
||||||
`fix` и `chore`, и **цель им не требуется**: они служат работоспособности, а
|
`fix` и `chore`, и **цель им не требуется**: они служат работоспособности, а
|
||||||
не направлению, и в спринт входят помимо его цели. Придуманная им цель —
|
не направлению, и в спринт входят помимо его цели. Придуманная им цель —
|
||||||
ровно то враньё, от которого спасает род работы.
|
ровно то враньё, от которого спасает тип.
|
||||||
|
|
||||||
Цель обязательна у находки, которая оказалась **новой возможностью**
|
Цель обязательна у находки, которая оказалась **новой возможностью**
|
||||||
(`kind:feature`): нашлось поведение, которого никто не заказывал, и его надо
|
(`feature`): нашлось поведение, которого никто не заказывал, и его надо
|
||||||
либо заказать целью, либо убрать. Подходящей цели нет — заведи её
|
либо заказать целью, либо убрать. Подходящей цели нет — заведи её
|
||||||
(`add --type goal --section Направления`) в том же проходе.
|
(`add --type goal --section Направления`) в том же проходе.
|
||||||
5. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в
|
5. **Покажи карту до создания файлов.** Кластер → задача / сырьё / строка в
|
||||||
пакетный файл / уже заведено / отброшено, и под какую цель — пачкой через
|
пакетный файл / уже заведено / отброшено, и под какую цель — пачкой через
|
||||||
`AskUserQuestion`. Это тот же барьер, что и «три кандидата» в интейке из
|
`AskUserQuestion`. Это тот же барьер, что и «три кандидата» в интейке из
|
||||||
диалога: массовое заведение файлов без подтверждения — ровно тот отказ, ради
|
диалога: массовое заведение файлов без подтверждения — ровно тот отказ, ради
|
||||||
@@ -58,13 +61,14 @@
|
|||||||
заводиться и без поштучного вопроса — но карта пользователю предъявляется
|
заводиться и без поштучного вопроса — но карта пользователю предъявляется
|
||||||
всё равно.
|
всё равно.
|
||||||
6. **Заводи утверждённое** через `tasks.py add`, с тремя добавками:
|
6. **Заводи утверждённое** через `tasks.py add`, с тремя добавками:
|
||||||
- **тег партии** — `--tag review-ГГГГ-ММ-ДД` (или `audit-<тема>`), чтобы весь
|
- **тег партии** — `--tag review-ГГГГ-ММ-ДД` (или `audit-<slug>`), чтобы весь
|
||||||
заход разбора поднимался одной командой `list --tag …`;
|
заход разбора поднимался одной командой `list --tag …`;
|
||||||
- **род работы** — `--kind`. У находок ревью он **не по умолчанию `fix`**:
|
- **тип** — `--type`, и он **не по умолчанию `fix`**: починкой считается
|
||||||
починкой считается расхождение с заявленным поведением, а находка «этого
|
расхождение с заявленным поведением, а находка «этого свойства никто не
|
||||||
свойства никто не заказывал» — это `feature`, находка «не знаем, как
|
заказывал» — это `feature`, находка «не знаем, как поведёт себя драйвер» —
|
||||||
поведёт себя драйвер» — `research`. Род, розданный оптом, врёт ровно там,
|
`research`. Тип, розданный оптом, врёт ровно там, где по нему потом
|
||||||
где по нему потом отбирают;
|
отбирают, **и требует не тех разделов**: каждому `fix` придётся заполнить
|
||||||
|
`Воспроизведение`, а у находки без свидетельства его нет;
|
||||||
- **провенанс в теле** — кто нашёл, каким проходом, с каким свидетельством.
|
- **провенанс в теле** — кто нашёл, каким проходом, с каким свидетельством.
|
||||||
Без него через месяц не отличить проверенную находку от догадки.
|
Без него через месяц не отличить проверенную находку от догадки.
|
||||||
7. `tasks.py check`.
|
7. `tasks.py check`.
|
||||||
@@ -82,7 +86,8 @@
|
|||||||
- **находка, ломающая уже идущий спринт**, — не интейк вовсе: см. правило
|
- **находка, ломающая уже идущий спринт**, — не интейк вовсе: см. правило
|
||||||
вторжения в скилле `session`. В беклог она падает, только если врываться не
|
вторжения в скилле `session`. В беклог она падает, только если врываться не
|
||||||
положено;
|
положено;
|
||||||
- **низкая уверенность или нет свидетельства** → идея;
|
- **низкая уверенность или нет свидетельства** → сырьё (`research` с пустым
|
||||||
|
разделом «Вопрос»);
|
||||||
- **мелочь** → строка в пакетный файл;
|
- **мелочь** → строка в пакетный файл;
|
||||||
- **уже починено / развилка решена сейчас** → ничего.
|
- **уже починено / развилка решена сейчас** → ничего.
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@
|
|||||||
|
|
||||||
- Источник (какое ревью/аудит, сколько находок на входе).
|
- Источник (какое ревью/аудит, сколько находок на входе).
|
||||||
- Свёрнуто в задачи: N кластеров из M находок, со слагами, целями и тегом партии.
|
- Свёрнуто в задачи: N кластеров из M находок, со слагами, целями и тегом партии.
|
||||||
- Что не заведено и почему: починено инлайн, уже заведено, ушло в идеи, в
|
- Что не заведено и почему: починено инлайн, уже заведено, стало сырьём, ушло в
|
||||||
`REJECTED.md`.
|
`REJECTED.md`.
|
||||||
- Поимённая сверка: находок на входе N, исход есть у N.
|
- Поимённая сверка: находок на входе N, исход есть у N.
|
||||||
- `tasks.py check`.
|
- `tasks.py check`.
|
||||||
|
|||||||
@@ -57,10 +57,10 @@
|
|||||||
через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на
|
через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на
|
||||||
наследников, а не археологией git;
|
наследников, а не археологией git;
|
||||||
- родитель осмыслен как **возможность**, а не как шаг → это цель. Тип на месте
|
- родитель осмыслен как **возможность**, а не как шаг → это цель. Тип на месте
|
||||||
не меняется (цель живёт в другом индексе): заводится `[goal]` в `ROADMAP.md`,
|
не меняется (цель живёт в другом индексе): заводится `--type goal` в `ROADMAP.md`,
|
||||||
части получают `--goal <новый слаг>`, родитель закрывается с причиной-ссылкой.
|
части получают `--goal <новый слаг>`, родитель закрывается с причиной-ссылкой.
|
||||||
|
|
||||||
**Промежуточного зонтика между целью и задачей нет.** Тип `[epic]` упразднён:
|
**Промежуточного зонтика между целью и задачей нет.** Тип `epic` упразднён:
|
||||||
роль зонтика играет цель, а слишком крупный шаг дробится на шаги помельче под
|
роль зонтика играет цель, а слишком крупный шаг дробится на шаги помельче под
|
||||||
той же целью. Если частям нужен общий заголовок — значит у них общая
|
той же целью. Если частям нужен общий заголовок — значит у них общая
|
||||||
возможность, и её надо назвать целью, а не заводить временный тип.
|
возможность, и её надо назвать целью, а не заводить временный тип.
|
||||||
@@ -73,10 +73,15 @@
|
|||||||
спринт продолжается остальными. Части заводятся сразу под той же целью, но в
|
спринт продолжается остальными. Части заводятся сразу под той же целью, но в
|
||||||
текущий набор **не добавляются** — набор заморожен.
|
текущий набор **не добавляются** — набор заморожен.
|
||||||
|
|
||||||
## Мозговой штурм идеи
|
## Мозговой штурм сырья
|
||||||
|
|
||||||
Идея (`[idea]`) не проходит тест «готова к взятию»: неясно, что именно делаем.
|
Сырьё — запись типа `research`, у которой раздел «Вопрос» пуст: она не проходит
|
||||||
Штурм проясняет — и это **generative-операция, а не applicative**.
|
тест «готова к взятию», потому что неясно, что именно делаем. Штурм проясняет —
|
||||||
|
и это **generative-операция, а не applicative**.
|
||||||
|
|
||||||
|
Исход штурма и есть заполненный «Вопрос» (тогда разведку можно брать в спринт)
|
||||||
|
или набор задач с типами, которые из ответа следуют. Третий законный исход —
|
||||||
|
`close --reason`.
|
||||||
|
|
||||||
Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное:
|
Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное:
|
||||||
перечисляется то, что уже видно в формулировке. Ценное — на уровень выше.
|
перечисляется то, что уже видно в формулировке. Ценное — на уровень выше.
|
||||||
@@ -87,9 +92,9 @@ Applicative-штурм («перечисли задачи, следующие и
|
|||||||
applicative.
|
applicative.
|
||||||
2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку
|
2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку
|
||||||
выбирает он: это продуктовое решение, не механика.
|
выбирает он: это продуктовое решение, не механика.
|
||||||
3. **Назови цель.** Выбранная форма служит какой-то цели — существующей или
|
3. **Назови цель.** Выбранная форма служит цели — существующей или новой. Идея,
|
||||||
новой. Идея, для которой цель не находится, скорее всего уезжает в
|
для которой цель не находится, скорее всего уезжает в `REJECTED.md`, а не
|
||||||
`REJECTED.md`, а не заводится задачей.
|
заводится задачей.
|
||||||
4. **Только выбранную форму** дроби по тесту декомпозиции выше и проставь
|
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` — руками их не
|
Заголовок, мета-блок и строку индекса ставит `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`:
|
`items/<slug>.md`:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# Тай-брейк при равной полноте
|
# 🐞 Не отбрасывать молча лишние символы в ходе
|
||||||
|
|
||||||
- **Секция:** Ядро — вышла из спринта: остаток писал нерешённое в журнал
|
- **Тип:** fix
|
||||||
- **Зачем:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт
|
- **Категория:** Ядро — вышла из спринта: остаток писал нерешённое в журнал
|
||||||
- **Теги:** goal:merge-robustness, kind:fix, sprint:2026-08-03
|
- **Зачем:** ввод «а1б2» ходит в a1 — игрок не видит, что ошибся, и винит игру
|
||||||
|
- **Теги:** goal:merge-robustness, sprint:2026-08-03
|
||||||
|
|
||||||
При столкновении точек выигрывает более полная, но при равной полноте побеждает
|
Разбор хода читает первые два символа и молча выбрасывает остаток строки.
|
||||||
последняя доставка — а она систематически беднее первой.
|
|
||||||
|
## Воспроизведение
|
||||||
|
|
||||||
|
Ввести `а1б2` в свой ход: программа ходит в `a1` и ничего не сообщает.
|
||||||
|
Ожидалось — отказ с ошибкой разбора.
|
||||||
|
|
||||||
## Затрагивает
|
## Затрагивает
|
||||||
|
|
||||||
Таблица `points` и её миграция; правило слияния в приёме доставки; формат
|
Разбор строки хода; текст ошибки в выводе партии. Формат сохранения партии
|
||||||
отпечатка состояния на диске. Публичного контракта не трогает.
|
не трогается.
|
||||||
|
|
||||||
## Критерии приёмки
|
## Критерии приёмки
|
||||||
|
|
||||||
- повторный прогон свёртки даёт тот же отпечаток состояния — оракул: команда сверки
|
- ввод «а1б2» отвергается с ошибкой — оракул: тест разбора
|
||||||
- накопительная метрика за сутки не уменьшается после повторной доставки — оракул: тест
|
- ввод «а1» принимается по-прежнему — оракул: тест разбора
|
||||||
- в логе видно, какая из двух точек выиграла и почему — оракул: глазами по логу прогона
|
|
||||||
|
|
||||||
## Рамки
|
## Рамки
|
||||||
|
|
||||||
Схема не трогается; данные только читаются; перезапуск сервиса допустим.
|
Схема не трогается; данные только читаются; перезапуск допустим.
|
||||||
|
|
||||||
Связано: решение о канонической форме содержимого.
|
Связано: решение о канонической форме содержимого.
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
|
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Начинается
|
||||||
префиксом `[goal]` / `[idea]`; обычная задача — без префикса.
|
**эмодзи типа**, и она **производна**: её ставит `add` и чинит `check --fix`
|
||||||
Отдельного поля типа **нет**: два места для одного факта разъезжаются, а
|
по полю меты. Второго дома у типа нет — эмодзи это его отображение, как
|
||||||
префикс виден прямо в индексе, где и принимается решение «брать или не брать».
|
строка индекса это отображение файла.
|
||||||
- **Форма заголовка — по типу записи.** Задача отвечает на «что нужно сделать»
|
- **Форма заголовка — по типу.** Цель отвечает на «что приложение будет уметь»;
|
||||||
и пишется глаголом в неопределённой форме («Печатать поле одним куском кода»,
|
`feature`, `fix` и `chore` — на «что нужно сделать», глаголом в неопределённой
|
||||||
«Не отбрасывать молча лишние символы»); цель — на «что приложение будет
|
форме, перед ним допускается «не»; `research` называет предмет разведки и
|
||||||
уметь»; идея просто называет, о чём она. Почему так — SKILL.md, «Как написана
|
формы действия **не несёт намеренно**. Почему так — SKILL.md, «Как написана
|
||||||
задача». `check` считает заголовки не в форме действия и печатает число в
|
задача». `check` считает заголовки не в форме действия и печатает число в
|
||||||
здоровье; годность формулировки смотрит агент `task-form`.
|
здоровье; годность формулировки смотрит агент `task-form`.
|
||||||
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательна
|
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательны
|
||||||
секция, причина после тире желательна (именно она объясняет, почему задача
|
**тип** и **место**, причина после тире желательна (именно она объясняет,
|
||||||
здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и теги
|
почему задача здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и
|
||||||
опциональны. Порядок свободный, поле в одну строку. Нераспознанные поля
|
теги необязательны. Нераспознанные поля сохраняются: скрипт правит свои и не
|
||||||
сохраняются: скрипт правит свои и не трогает чужие.
|
трогает чужие.
|
||||||
|
- **Тип — первым полем.** Он решает, что у записи вообще может быть: какие
|
||||||
|
разделы обязательны, нужна ли цель, берётся ли она в спринт, — и читается
|
||||||
|
раньше всего остального. Словарь **закрыт**: `goal` | `feature` | `fix` |
|
||||||
|
`chore` | `research`. Не подходит ни один — это сигнал, что в записи их два и
|
||||||
|
её надо разделить.
|
||||||
- **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль.
|
- **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль.
|
||||||
Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в
|
Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в
|
||||||
индексе: строка индекса его повторяет и производна от него, `check` сверяет,
|
индексе: строка индекса его повторяет и производна от него, `check` сверяет,
|
||||||
`check --fix` восстанавливает пропавшую строку **вместе с ним**. Пока поле
|
`check --fix` восстанавливает пропавшую строку **вместе с ним**. Пока поле
|
||||||
лежало только в индексе, штатная починка дрейфа теряла его молча и
|
лежало только в индексе, штатная починка дрейфа теряла его молча и
|
||||||
навсегда — а это единственное, по чему задачу выбирают, не открывая.
|
навсегда — а это единственное, по чему задачу выбирают, не открывая.
|
||||||
|
- **Тело** — одна фраза «что станет наблюдаемо иначе», дальше разделы по схеме
|
||||||
- **Тело** — одна фраза «что станет наблюдаемо иначе», затрагиваемые границы,
|
типа. Пишется на языке документации проекта: предметно, без англицизмов, у
|
||||||
критерии приёмки, рамки, контекст, ссылки. Пишется на языке документации
|
которых есть русское слово, и без терминов, которых нет ни в паспорте, ни в
|
||||||
проекта: предметно, без англицизмов, у которых есть русское слово, и без
|
архитектуре, ни в конвенциях (правило и его причина — в SKILL.md, раздел «Как
|
||||||
терминов, которых нет ни в паспорте, ни в архитектуре, ни в конвенциях
|
написана задача»).
|
||||||
(правило и его причина — в SKILL.md, раздел «Как написана задача»).
|
|
||||||
|
|
||||||
Мета **одной строкой через `·`** — прежняя форма. Она читается по-прежнему,
|
|
||||||
`check` называет её дрейфом, `check --fix` переписывает списком; поле `Хук`
|
|
||||||
при этом становится `Зачем`. Причина отказа от строки простая: с тремя полями
|
|
||||||
и длинным «зачем» строка уезжала за экран, а `·` приходилось запрещать в тексте
|
|
||||||
причины и самого «зачем».
|
|
||||||
|
|
||||||
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
|
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
|
||||||
в документацию проекта, а файл задачи удаляется.
|
в документацию проекта, а файл задачи удаляется.
|
||||||
|
|
||||||
|
### Поле места: «Категория» и «Секция»
|
||||||
|
|
||||||
|
Поле называет, **где числится строка**, и имя у него **зависит от типа**:
|
||||||
|
|
||||||
|
| Тип | Поле | Значения | Что это |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `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` о нём скажет.
|
`question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет.
|
||||||
|
|
||||||
|
Раздел `Вопросы` (о решении человека) и раздел `Вопрос` у `research` (предмет
|
||||||
|
разведки) — **разные вещи и разные слова**: первый блокирует взятие, второй его
|
||||||
|
разрешает.
|
||||||
|
|
||||||
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
|
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
|
||||||
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
|
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
|
||||||
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
|
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
|
||||||
@@ -158,13 +212,12 @@
|
|||||||
|
|
||||||
## Файл цели
|
## Файл цели
|
||||||
|
|
||||||
**Заголовок цели отвечает на «что приложение будет уметь».** Не область работ и
|
Форма та же, разделы и алгоритм — [task-goal.md](task-goal.md).
|
||||||
не имя подсистемы: не «Работа со слиянием», а «Исход слияния не зависит от
|
|
||||||
порядка доставки». Свойство поведения — тоже возможность.
|
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# [goal] Исход слияния не зависит от порядка доставки
|
# 🎯 Исход слияния не зависит от порядка доставки
|
||||||
|
|
||||||
|
- **Тип:** goal
|
||||||
- **Секция:** Направления
|
- **Секция:** Направления
|
||||||
- **Теги:** decomposed
|
- **Теги:** decomposed
|
||||||
|
|
||||||
@@ -181,10 +234,6 @@
|
|||||||
- **Задачи цели здесь не перечисляются.** Перечень даёт
|
- **Задачи цели здесь не перечисляются.** Перечень даёт
|
||||||
`tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и
|
`tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и
|
||||||
поехал бы на первой же закрытой задаче.
|
поехал бы на первой же закрытой задаче.
|
||||||
- **Раздел «Завершение» — списком, а не абзацем.** Это признаки того, что
|
|
||||||
приложение уже умеет; **на строку «Завершения» ссылается задача**, объясняя,
|
|
||||||
какую часть возможности она двигает (см. тест готовности). Абзацем такая
|
|
||||||
ссылка не берётся, поэтому список.
|
|
||||||
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
|
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
|
||||||
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
|
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
|
||||||
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
|
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
|
||||||
@@ -217,16 +266,18 @@
|
|||||||
Строка везде одной формы:
|
Строка везде одной формы:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
- [Заголовок дословно](items/slug.md) — зачем
|
- [🐞 Заголовок дословно](items/slug.md) — зачем
|
||||||
```
|
```
|
||||||
|
|
||||||
«Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние,
|
«Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние,
|
||||||
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
|
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
|
||||||
|
Эмодзи внутри квадратных скобок не украшение: заголовок копируется **дословно**,
|
||||||
|
и тип виден там, где решают «брать или не брать».
|
||||||
|
|
||||||
| Файл | Что отвечает | Секции |
|
| Файл | Что отвечает | Секции |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические: `Готово`, `Запланировано`, `Направления`, `Разработка` (англ. `Done`, `Planned`, `Directions`, `Tooling`) |
|
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические и в этом порядке: `Запланировано`, `Направления`, `Сопровождение`, `Готово` (англ. `Planned`, `Directions`, `Operations`, `Done`) |
|
||||||
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию Ядро/Инфра) |
|
| `BACKLOG.md` | что **можно взять** — только задачи | категории проекта (по умолчанию Ядро/Инфра) |
|
||||||
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
||||||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||||||
|
|
||||||
@@ -237,8 +288,15 @@
|
|||||||
следующем `sprint start` и очищается на `sprint close`.
|
следующем `sprint start` и очищается на `sprint close`.
|
||||||
|
|
||||||
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
|
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
|
||||||
преамбуле проверка сочтёт секцией. Внутри секции беклога порядок значения не
|
преамбуле проверка сочтёт секцией.
|
||||||
имеет — порядка в беклоге нет вовсе.
|
|
||||||
|
**Порядка «по важности» внутри секции беклога нет** — «что делать дальше»
|
||||||
|
отвечает набор спринта. Единственный порядок, который есть, **производен от типа
|
||||||
|
и заполненности**: **сырьё** (`research` без раздела «Вопрос») стоит в конце
|
||||||
|
своей секции, потому что его не берут, и между берущимся оно каждый раз требует
|
||||||
|
открыть файл, чтобы это понять. Проверяет `check`, переставляет `check --fix`,
|
||||||
|
и человек этот порядок не назначает — иначе он был бы приоритетом, которого
|
||||||
|
здесь нет.
|
||||||
|
|
||||||
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
|
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
|
||||||
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
|
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
|
||||||
@@ -250,14 +308,15 @@
|
|||||||
строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в
|
строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в
|
||||||
`REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда).
|
`REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда).
|
||||||
|
|
||||||
**Секции роадмапа закреплены** — состав, полнота и единство языка проверяются
|
**Секции роадмапа закреплены** — состав, полнота, единство языка и **порядок**
|
||||||
`check`; секции беклога проект называет сам. Почему так — SKILL.md.
|
проверяются `check`; категории беклога проект называет сам. Почему так —
|
||||||
|
SKILL.md. Порядок закреплён потому, что `Готово` копится: стоя первым,
|
||||||
|
достигнутое отодвигает за экран то, ради чего роадмап открывают чаще всего.
|
||||||
|
|
||||||
**Заголовок секции пишется с прописной, и после него идёт пустая строка** — во
|
**Заголовок секции пишется с прописной и отбивается пустой строкой с обеих
|
||||||
всех индексах, включая секции беклога, имена которых выбирает проект. Написание
|
сторон** — во всех индексах, включая категории беклога, имена которых выбирает
|
||||||
канонических секций и отбивку правит `check --fix`; он же сводит написание
|
проект. Написание канонических секций и отбивку правит `check --fix`; он же
|
||||||
секции в мете файла с заголовком индекса — **имя секции принадлежит заголовку**,
|
сводит написание места в мете файла с заголовком индекса.
|
||||||
файл на неё лишь ссылается, и принадлежность сверяется по нижнему регистру.
|
|
||||||
|
|
||||||
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
|
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
|
||||||
Строку руками не пишут.
|
Строку руками не пишут.
|
||||||
@@ -293,19 +352,13 @@
|
|||||||
|
|
||||||
## Теги
|
## Теги
|
||||||
|
|
||||||
Единственный механизм разметки, потому что `list --tag` уже умеет отбирать по
|
Разметка сверх типа. Тип полем, потому что он один и обязателен; теги — потому
|
||||||
ним порцию разбора. Отдельных полей меты под это не заводим.
|
что их много и `list --tag` уже умеет отбирать по ним порцию разбора.
|
||||||
|
|
||||||
- `goal:<слаг>` — цель, которой служит задача. Обязателен **у `kind:feature`**:
|
- `goal:<слаг>` — цель, которой служит задача. Обязателен **у `feature`**:
|
||||||
новая возможность и есть содержание цели. У `fix`, `chore` и `research` его
|
новая возможность и есть содержание цели. У `fix`, `chore` и `research` его
|
||||||
может не быть — они служат работоспособности, а не направлению, и в набор
|
может не быть — они служат работоспособности, а не направлению, и в набор
|
||||||
спринта входят помимо его цели.
|
спринта входят помимо его цели.
|
||||||
- `kind:<род>` — род работы: `feature` | `fix` | `chore` | `research`. Словарь
|
|
||||||
**закрыт**, значение ровно одно. Обязателен у задачи (без него `sprint take`
|
|
||||||
откажет), у цели запрещён, у идеи необязателен. Ставится
|
|
||||||
`add --kind` / `edit --kind`; `--kind` заменяет прежнее значение, а не
|
|
||||||
добавляет второе. Смысл рода и почему он тегом, а не префиксом — в SKILL.md,
|
|
||||||
раздел «Род работы».
|
|
||||||
- `question` — в файле есть неразобранный раздел «Вопросы».
|
- `question` — в файле есть неразобранный раздел «Вопросы».
|
||||||
- `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая
|
- `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая
|
||||||
порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит
|
порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит
|
||||||
@@ -315,6 +368,9 @@
|
|||||||
разбора — урожай прошедшего спринта».
|
разбора — урожай прошедшего спринта».
|
||||||
- `decomposed` — на цели: разложена на задачи (см. «Файл цели»).
|
- `decomposed` — на цели: разложена на задачи (см. «Файл цели»).
|
||||||
|
|
||||||
|
Тега `kind:<род>` больше нет: род работы стал типом. Оставшийся в файле `check`
|
||||||
|
называет дрейфом, а `check --fix` снимает, перенеся значение в поле «Тип».
|
||||||
|
|
||||||
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
|
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
|
||||||
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
|
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
|
||||||
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
|
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
|
||||||
@@ -325,17 +381,20 @@
|
|||||||
|
|
||||||
## Тест «готова к взятию»
|
## Тест «готова к взятию»
|
||||||
|
|
||||||
Задача готова, если из файла отвечаются четыре вопроса:
|
Задача готова, если из файла отвечаются четыре вопроса. Первый и четвёртый —
|
||||||
|
общие, второй и третий у каждого типа свои и перечислены в его файле.
|
||||||
|
|
||||||
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
|
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
|
||||||
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
|
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
|
||||||
ломаться Y при Z» — ответ. **У `kind:chore` адресат — разработчик, и это
|
ломаться Y при Z» — ответ. **У `chore` адресат — разработчик, и это
|
||||||
законно**: «уедет последний вызов устаревшего API» — ответ, а не отговорка.
|
законно**: «уедет последний вызов устаревшего API» — ответ, а не отговорка.
|
||||||
Род объявлен как раз затем, чтобы такие задачи не выдумывали себе
|
Тип объявлен как раз затем, чтобы такие задачи не выдумывали себе
|
||||||
пользовательскую пользу.
|
пользовательскую пользу.
|
||||||
2. **Каких границ это касается** — раздел «Затрагивает». Без него задачу нельзя
|
2. **Что известно про сегодня** — то, что тип требует знать до работы:
|
||||||
оценить: остаётся судить по длине текста.
|
у `fix` это `Воспроизведение`, у `research` — `Вопрос`, у `feature` и
|
||||||
3. **По чему видно, что закончено** — критерии приёмки с оракулами.
|
`chore` — `Затрагивает`.
|
||||||
|
3. **По чему видно, что закончено** — критерии приёмки с оракулами;
|
||||||
|
у `research` вместо них `Куда ляжет ответ`.
|
||||||
4. **Какую часть «Завершения» своей цели она двигает** — у задачи с целью.
|
4. **Какую часть «Завершения» своей цели она двигает** — у задачи с целью.
|
||||||
Строкой: «двигает пункт 2 «Завершения» — накопительная метрика перестаёт
|
Строкой: «двигает пункт 2 «Завершения» — накопительная метрика перестаёт
|
||||||
уменьшаться». Это и есть защита от задачи «отрефакторить X»: она проваливает
|
уменьшаться». Это и есть защита от задачи «отрефакторить X»: она проваливает
|
||||||
@@ -347,9 +406,10 @@
|
|||||||
**У задачи без цели** (`fix`, `chore`, `research`) вопрос не задаётся: они
|
**У задачи без цели** (`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]
|
[tool.ruff]
|
||||||
target-version = "py312"
|
target-version = "py312"
|
||||||
line-length = 88
|
line-length = 88
|
||||||
# backlog.py заморожен: плагин помечен УСТАРЕЛ и живёт до перевода последнего
|
|
||||||
# проекта, после чего удаляется целиком. Правки в него — риск без выгоды.
|
|
||||||
exclude = ["av-dev-backlog"]
|
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = [
|
select = [
|
||||||
|
|||||||
+6
-3
@@ -15,11 +15,14 @@
|
|||||||
|
|
||||||
<!-- дом: <id> -->
|
<!-- дом: <id> -->
|
||||||
…текст…
|
…текст…
|
||||||
<!-- /дом -->
|
<!-- /дом: <id> -->
|
||||||
|
|
||||||
<!-- копия: <id> из <путь к файлу дома> -->
|
<!-- копия: <id> из <путь к файлу дома> -->
|
||||||
…тот же текст…
|
…тот же текст…
|
||||||
<!-- /копия -->
|
<!-- /копия: <id> -->
|
||||||
|
|
||||||
|
Закрывающий маркер несёт **тот же id**, что открывающий: без него не отличить
|
||||||
|
конец своего блока от конца соседнего, а вложенных блоков разметка не знает.
|
||||||
|
|
||||||
Сверяется текст **между** маркерами: построчно, с отброшенными хвостовыми
|
Сверяется текст **между** маркерами: построчно, с отброшенными хвостовыми
|
||||||
пробелами и пустыми строками по краям. Всё остальное вокруг копии — предисловие,
|
пробелами и пустыми строками по краям. Всё остальное вокруг копии — предисловие,
|
||||||
@@ -43,7 +46,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
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