«Разработка» называла слишком много: роадмап весь про разработку, и секция с таким именем не отличалась от остальных ничем. Стало Сопровождение | Operations. Смысл расширен вместе с именем: было «инструмент и процесс», стало «чем держат проект: инструмент, процесс, эксплуатация». Расширение не косметическое — английское Operations при узком смысле обещало бы эксплуатацию, а внутри лежал бы линтер. Метрики, логи, инфраструктура и выкладка в эту секцию просятся и так. Заодно синхронизирован словарь трёх мест канона, которые про одну тему. Сопровождение — всё, чем держат проект; эксплуатация — его часть, работа системы на проде. ROADMAP.md, секция Сопровождение — план работ; architecture.md, раздел «Эксплуатация» — как устроено сейчас; эксплуатационный проход ревью — оптика проверки. Сливать их в одно слово было бы ошибкой: они отвечают на разные вопросы. Синхронизирован словарь, а не границы; дом — canon.md. Слово «поддержка» запрещено вовсе: в нём слышится помощь пользователю. Граница с возможностями проходит по тому, кто наблюдает: «приложение сообщает о своём состоянии» — возможность, «дежурный видит состояние на одном экране» — сопровождение. Версия канона не менялась, и это законно: ни один проект на каноне 3 не стоит, оба держат канон 2. Запись версии 3 правится как черновик, а не как история — версия отделяет одно состояние проектов от другого, а не одну редакцию текста от другой. DECISIONS тема 25 (ЧЧЧ, ШШШ, ЩЩЩ, следствия 96–97). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
362 lines
30 KiB
Markdown
362 lines
30 KiB
Markdown
# Формат задач, целей и индексов
|
||
|
||
Заголовок, мета-блок и строку индекса ставит `tasks.py add` — руками их не
|
||
пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`;
|
||
тело задачи (одну фразу, критерии, рамки, контекст) дописывает агент.
|
||
|
||
## Файл задачи
|
||
|
||
`items/<slug>.md`:
|
||
|
||
```markdown
|
||
# Тай-брейк при равной полноте
|
||
|
||
- **Секция:** Ядро — вышла из спринта: остаток писал нерешённое в журнал
|
||
- **Зачем:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт
|
||
- **Теги:** goal:merge-robustness, kind:fix, sprint:2026-08-03
|
||
|
||
При столкновении точек выигрывает более полная, но при равной полноте побеждает
|
||
последняя доставка — а она систематически беднее первой.
|
||
|
||
## Затрагивает
|
||
|
||
Таблица `points` и её миграция; правило слияния в приёме доставки; формат
|
||
отпечатка состояния на диске. Публичного контракта не трогает.
|
||
|
||
## Критерии приёмки
|
||
|
||
- повторный прогон свёртки даёт тот же отпечаток состояния — оракул: команда сверки
|
||
- накопительная метрика за сутки не уменьшается после повторной доставки — оракул: тест
|
||
- в логе видно, какая из двух точек выиграла и почему — оракул: глазами по логу прогона
|
||
|
||
## Рамки
|
||
|
||
Схема не трогается; данные только читаются; перезапуск сервиса допустим.
|
||
|
||
Связано: решение о канонической форме содержимого.
|
||
```
|
||
|
||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
|
||
префиксом `[goal]` / `[idea]`; обычная задача — без префикса.
|
||
Отдельного поля типа **нет**: два места для одного факта разъезжаются, а
|
||
префикс виден прямо в индексе, где и принимается решение «брать или не брать».
|
||
- **Форма заголовка — по типу записи.** Задача отвечает на «что нужно сделать»
|
||
и пишется глаголом в неопределённой форме («Печатать поле одним куском кода»,
|
||
«Не отбрасывать молча лишние символы»); цель — на «что приложение будет
|
||
уметь»; идея просто называет, о чём она. Почему так — SKILL.md, «Как написана
|
||
задача». `check` считает заголовки не в форме действия и печатает число в
|
||
здоровье; годность формулировки смотрит агент `task-form`.
|
||
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательна
|
||
секция, причина после тире желательна (именно она объясняет, почему задача
|
||
здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и теги
|
||
опциональны. Порядок свободный, поле в одну строку. Нераспознанные поля
|
||
сохраняются: скрипт правит свои и не трогает чужие.
|
||
- **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль.
|
||
Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в
|
||
индексе: строка индекса его повторяет и производна от него, `check` сверяет,
|
||
`check --fix` восстанавливает пропавшую строку **вместе с ним**. Пока поле
|
||
лежало только в индексе, штатная починка дрейфа теряла его молча и
|
||
навсегда — а это единственное, по чему задачу выбирают, не открывая.
|
||
|
||
- **Тело** — одна фраза «что станет наблюдаемо иначе», затрагиваемые границы,
|
||
критерии приёмки, рамки, контекст, ссылки. Пишется на языке документации
|
||
проекта: предметно, без англицизмов, у которых есть русское слово, и без
|
||
терминов, которых нет ни в паспорте, ни в архитектуре, ни в конвенциях
|
||
(правило и его причина — в SKILL.md, раздел «Как написана задача»).
|
||
|
||
Мета **одной строкой через `·`** — прежняя форма. Она читается по-прежнему,
|
||
`check` называет её дрейфом, `check --fix` переписывает списком; поле `Хук`
|
||
при этом становится `Зачем`. Причина отказа от строки простая: с тремя полями
|
||
и длинным «зачем» строка уезжала за экран, а `·` приходилось запрещать в тексте
|
||
причины и самого «зачем».
|
||
|
||
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
|
||
в документацию проекта, а файл задачи удаляется.
|
||
|
||
### Затрагивает
|
||
|
||
Перечень **границ**, которых изменение касается. Границей считается то, у чего
|
||
есть внешняя сторона и цена изменения:
|
||
|
||
- эндпоинт, команда, форма ответа, код ответа;
|
||
- таблица, поле, миграция, формат на диске, формат сообщения в очереди;
|
||
- публичный тип или функция пакета, конфиг и его образцы;
|
||
- внешний сервис или библиотека, чьё поведение становится нужным.
|
||
|
||
Ничего из этого не трогается — так и пишется: «границ не трогает, изменение
|
||
внутри одного узла». Это ответ, а не пустой раздел.
|
||
|
||
**Границы, а не замысел.** «Переписать хранилище на новый драйвер» — замысел;
|
||
`таблица points и её миграция`, `эндпоинт POST /ingest` — границы. Разница
|
||
проверяется вопросом «это можно назвать до того, как решено *как* делать?»: если
|
||
нет, строка описывает реализацию, и её место в предложении об изменении.
|
||
|
||
**Свойства репозитория сюда не пишутся** — по той же причине, что и в рамки:
|
||
имя таблицы стабильно, номер последней миграции протухает молча. Пишется
|
||
`таблица points и её миграция`, а не `миграция 0042`.
|
||
|
||
**Что из этого механизировано.** `check` и `sprint take` смотрят только на
|
||
**наличие непустого раздела**. Полнота перечня машине не видна: границу, которую
|
||
забыли назвать, она от отсутствующей не отличает. Раздела нет — отказ во взятии:
|
||
оценивать нечем.
|
||
|
||
**У идей раздела нет** — как и критериев: границы становятся известны, когда идея
|
||
превращается в задачу.
|
||
|
||
### Критерии приёмки
|
||
|
||
2–5 проверяемых утверждений **списком** `- …`, **у каждого назван оракул**. Не
|
||
«работает корректно», а «повторный прогон даёт тот же отпечаток — оракул:
|
||
команда сверки». Это не второе определение готовности, а проектная
|
||
конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже:
|
||
там сказано «признак завершённости», здесь — «признак плюс чем проверяется».
|
||
|
||
**Что из этого механизировано.** `check` и `sprint take` считают пункты: меньше
|
||
двух — отказ («— работает» одной строкой больше не проходит), больше пяти —
|
||
замечание, обычно это признак, что задача крупнее задачи. Наличие оракула
|
||
проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только
|
||
замечание: настоящий оракул от слова «оракул» машина не отличает, и делать вид,
|
||
что проверено больше проверенного, хуже, чем не проверять вовсе.
|
||
|
||
**У идей критериев нет — именно поэтому они идеи.**
|
||
|
||
**Критерии — пол, но расхождение с ними есть дефект критериев.** Если приёмщик
|
||
видит, что критерии закрыты, а суть задачи не достигнута, он **правит критерии и
|
||
возвращает задачу исполнителю**, а не держит невидимое сверх-требование. Иначе
|
||
исполнитель никогда не знает, закончил ли, и мотивирован занижать критерии
|
||
заранее.
|
||
|
||
### Рамки
|
||
|
||
Одна строка: чего касаться нельзя, что перезапускается, что считается
|
||
необратимым, трогается ли схема данных. **Свойства репозитория сюда не пишутся**
|
||
— номер последней миграции, версия зависимости, хеш: в лежалой задаче они
|
||
протухают молча и становятся ложной рамкой. Снимок берётся при постановке, а не
|
||
при заведении.
|
||
|
||
### Вопросы
|
||
|
||
Неразобранное решение человека живёт разделом `## Вопросы` **плюс тегом
|
||
`question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет.
|
||
|
||
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
|
||
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
|
||
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
|
||
отбору снаружи файла (`list --questions`, `list --tag question`), и его
|
||
отсутствие при непустом разделе — замечание, а не лазейка. Тег без раздела тоже
|
||
отказ, но с другим советом: либо вопрос записан не туда, либо тег пора снять.
|
||
|
||
**Ответ на вопрос — три правки, и первая обязательна.** Раздел «Вопросы»
|
||
опустошается: ответ переезжает в тело решением, а не остаётся вопросом рядом с
|
||
ответом. Затем снимается тег (`edit <slug> --rm-tag question`) и переписывается
|
||
«зачем»: «Решено: …» на вопрос «зачем нужна эта задача» уже не отвечает.
|
||
|
||
**Порядок именно такой, потому что судит раздел, а не тег.** `sprint take`
|
||
смотрит в непустой раздел и откажет взять задачу даже со снятым тегом, а `check`
|
||
на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не
|
||
опустошив раздел, — значит закольцевать себя между двумя советами.
|
||
|
||
## Файл цели
|
||
|
||
**Заголовок цели отвечает на «что приложение будет уметь».** Не область работ и
|
||
не имя подсистемы: не «Работа со слиянием», а «Исход слияния не зависит от
|
||
порядка доставки». Свойство поведения — тоже возможность.
|
||
|
||
```markdown
|
||
# [goal] Исход слияния не зависит от порядка доставки
|
||
|
||
- **Секция:** Направления
|
||
- **Теги:** decomposed
|
||
|
||
Ради чего: точки из разных доставок сходятся в один часовой объект, и сегодня
|
||
исход столкновения зависит от порядка доставки, а не от содержания.
|
||
|
||
## Завершение
|
||
|
||
- повторная доставка тех же точек в другом порядке даёт то же состояние;
|
||
- накопительная метрика за сутки не уменьшается после повторной доставки;
|
||
- в логе видно, какая из двух точек выиграла и почему.
|
||
```
|
||
|
||
- **Задачи цели здесь не перечисляются.** Перечень даёт
|
||
`tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и
|
||
поехал бы на первой же закрытой задаче.
|
||
- **Раздел «Завершение» — списком, а не абзацем.** Это признаки того, что
|
||
приложение уже умеет; **на строку «Завершения» ссылается задача**, объясняя,
|
||
какую часть возможности она двигает (см. тест готовности). Абзацем такая
|
||
ссылка не берётся, поэтому список.
|
||
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
|
||
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
|
||
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
|
||
`check` напоминает о нём у цели без задач замечанием — неразобранная цель
|
||
законна и зелёного прогона не ломает; `check --fix` сам ставит его цели, у
|
||
которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть.
|
||
- Цель живёт в `ROADMAP.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`.
|
||
- **Достигнутая цель не исчезает.** `close <слаг> --implemented` удаляет файл и
|
||
переносит строку в секцию `Готово` с датой:
|
||
`- 2026-08-04 \`merge-order\` — Исход слияния не зависит от порядка доставки. …`
|
||
Ссылки на файл в ней нет — файл удалён, а битая ссылка это ошибка `check`.
|
||
Поведение живёт в спеках проекта; роадмап отвечает, **когда и в каком порядке**
|
||
оно появилось.
|
||
|
||
## Слаг
|
||
|
||
Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов
|
||
(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути, а не по текущей
|
||
формулировке**: заголовок будет переписан при переоценке, а слаг стоит в ссылках
|
||
из других задач, коммитов и черновиков. **Транслита не заводим** —
|
||
`tie-break-equal-completeness`, а не `taj-brejk-pri-ravnoj-polnote`: транслит
|
||
нечитаем для того, кто ищет по смыслу, и не сокращается.
|
||
|
||
Переименование слага — не правка, а перенос ссылок: делается одним атомарным
|
||
проходом по всем местам, где слаг упомянут, иначе останутся битые ссылки,
|
||
которых никто не проверяет.
|
||
|
||
## Индексы
|
||
|
||
Строка везде одной формы:
|
||
|
||
```markdown
|
||
- [Заголовок дословно](items/slug.md) — зачем
|
||
```
|
||
|
||
«Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние,
|
||
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
|
||
|
||
| Файл | Что отвечает | Секции |
|
||
| --- | --- | --- |
|
||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические: `Готово`, `Запланировано`, `Направления`, `Сопровождение` (англ. `Done`, `Planned`, `Directions`, `Operations`) |
|
||
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию Ядро/Инфра) |
|
||
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||
|
||
Шапку `SPRINT.md` пишет `sprint start` — **тем же мета-блоком, что у задачи**:
|
||
поле на строку, `- **Цель:** [Заголовок](items/slug.md)`, `- **Начат:**` датой,
|
||
`- **Спринт:**` слагом, которым метится урожай. Прежняя форма (три поля одной
|
||
строкой через `·`) читается по-прежнему и уходит сама: файл переписывается на
|
||
следующем `sprint start` и очищается на `sprint close`.
|
||
|
||
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
|
||
преамбуле проверка сочтёт секцией. Внутри секции беклога порядок значения не
|
||
имеет — порядка в беклоге нет вовсе.
|
||
|
||
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
|
||
ответа человека, а следы остаются вопросами в файлах задач распущенного спринта.
|
||
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
|
||
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
|
||
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В секции
|
||
**`Запланировано`** очередь значима и обосновывается прозой; двигают строку
|
||
`move <slug> --section Запланировано --after <другой>`. В секции **`Готово`**
|
||
строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в
|
||
`REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда).
|
||
|
||
**Секции роадмапа закреплены** — состав, полнота и единство языка проверяются
|
||
`check`; секции беклога проект называет сам. Почему так — SKILL.md.
|
||
|
||
**Заголовок секции пишется с прописной, и после него идёт пустая строка** — во
|
||
всех индексах, включая секции беклога, имена которых выбирает проект. Написание
|
||
канонических секций и отбивку правит `check --fix`; он же сводит написание
|
||
секции в мете файла с заголовком индекса — **имя секции принадлежит заголовку**,
|
||
файл на неё лишь ссылается, и принадлежность сверяется по нижнему регистру.
|
||
|
||
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
|
||
Строку руками не пишут.
|
||
|
||
Отсюда же ответ на «а если оборвётся посередине». Мутация сперва проверяет всё
|
||
и складывает правки, и только потом пишет: сначала все временные файлы, потом
|
||
переименования подряд. Полной транзакции на несколько файлов файловая система не
|
||
даёт, но окно сжато до цепочки переименований, а **всё, что в нём может
|
||
разъехаться, — производное**: файлы целы, индексы восстанавливает `check --fix`.
|
||
Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при
|
||
нетронутых индексах.
|
||
|
||
`SPRINT.md` и есть артефакт заморозки: без него набор существует только в
|
||
контексте сессии, и нарушение заморозки ненаблюдаемо.
|
||
|
||
## `REJECTED.md`
|
||
|
||
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
|
||
`tasks.py close --reason`, а `check` следит за форматом:
|
||
|
||
```markdown
|
||
- 2026-07-23 `versii-kachestvo-repaki` — Версии и качество одного тайтла.
|
||
Причина: калибровка болей — не боль, ни разу не возникло за полгода.
|
||
Была секция: Инфра.
|
||
```
|
||
|
||
Реализованные сюда не попадают: у них остаётся коммит и документация. У
|
||
выкинутой не остаётся ничего — и через квартал она возвращается тем же текстом.
|
||
Это первое место, куда смотрит дедупликация при заведении.
|
||
|
||
Запись не запрещает завести задачу заново: изменился контекст — заводим и
|
||
ссылаемся на строку, объясняя, что изменилось.
|
||
|
||
## Теги
|
||
|
||
Единственный механизм разметки, потому что `list --tag` уже умеет отбирать по
|
||
ним порцию разбора. Отдельных полей меты под это не заводим.
|
||
|
||
- `goal:<слаг>` — цель, которой служит задача. Обязателен **у `kind:feature`**:
|
||
новая возможность и есть содержание цели. У `fix`, `chore` и `research` его
|
||
может не быть — они служат работоспособности, а не направлению, и в набор
|
||
спринта входят помимо его цели.
|
||
- `kind:<род>` — род работы: `feature` | `fix` | `chore` | `research`. Словарь
|
||
**закрыт**, значение ровно одно. Обязателен у задачи (без него `sprint take`
|
||
откажет), у цели запрещён, у идеи необязателен. Ставится
|
||
`add --kind` / `edit --kind`; `--kind` заменяет прежнее значение, а не
|
||
добавляет второе. Смысл рода и почему он тегом, а не префиксом — в SKILL.md,
|
||
раздел «Род работы».
|
||
- `question` — в файле есть неразобранный раздел «Вопросы».
|
||
- `sprint:<слаг>` — задача заведена в этом спринте; по нему отбирается первая
|
||
порция разбора («урожай спринта»). **Ставится сам**: слаг спринта заводит
|
||
`sprint start` (по умолчанию — дата начала, он же пишется в `SPRINT.md`), и
|
||
`add` при открытом спринте помечает заводимое. Тег, который надо помнить
|
||
ставить руками, не ставится никогда — а на нём висит правило «первая порция
|
||
разбора — урожай прошедшего спринта».
|
||
- `decomposed` — на цели: разложена на задачи (см. «Файл цели»).
|
||
|
||
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
|
||
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
|
||
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
|
||
|
||
Свои теги проект заводит свободно (партия ревью `review-ГГГГ-ММ-ДД`, тема,
|
||
источник) — словарь не фиксирован. В индексы теги не выносим: индексы
|
||
производны, отбор делает `list --tag`, а не глаза.
|
||
|
||
## Тест «готова к взятию»
|
||
|
||
Задача готова, если из файла отвечаются четыре вопроса:
|
||
|
||
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
|
||
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
|
||
ломаться Y при Z» — ответ. **У `kind:chore` адресат — разработчик, и это
|
||
законно**: «уедет последний вызов устаревшего API» — ответ, а не отговорка.
|
||
Род объявлен как раз затем, чтобы такие задачи не выдумывали себе
|
||
пользовательскую пользу.
|
||
2. **Каких границ это касается** — раздел «Затрагивает». Без него задачу нельзя
|
||
оценить: остаётся судить по длине текста.
|
||
3. **По чему видно, что закончено** — критерии приёмки с оракулами.
|
||
4. **Какую часть «Завершения» своей цели она двигает** — у задачи с целью.
|
||
Строкой: «двигает пункт 2 «Завершения» — накопительная метрика перестаёт
|
||
уменьшаться». Это и есть защита от задачи «отрефакторить X»: она проваливает
|
||
тест не потому, что невидима снаружи, а потому, что не находит строки, к
|
||
которой относится. Заодно видно обратное — достаточен ли набор задач для
|
||
цели: строка «Завершения», к которой не относится ни одна задача, это
|
||
незакрытая часть возможности.
|
||
|
||
**У задачи без цели** (`fix`, `chore`, `research`) вопрос не задаётся: они
|
||
служат работоспособности, а не направлению.
|
||
|
||
Не отвечается первый, второй или третий вопрос → это **идея** (`[idea]`), её
|
||
место в штурме. Не отвечается четвёртый у `feature` → либо цель есть и не
|
||
проставлена, либо это не новая возможность.
|
||
|
||
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
|
||
это **несколько задач под одной целью**, дроби сразу. Промежуточного зонтика
|
||
между целью и задачей нет: тип `[epic]` упразднён, потому что зонтиком стала
|
||
сама цель.
|
||
|
||
Тест применяется при заведении и при переоценке. К старым задачам, которых
|
||
операция не касается, задним числом не применяется — беклог не переоформляют
|
||
«заодно».
|