роадмап — состояние проекта, а не очередь работ
Основной инструмент владельца отвечал на половину своего вопроса. Оценка идёт по поведению: что приложение уже может и чего ещё не может, — а close --implemented удалял у достигнутой цели и файл, и строку, так что роадмап по построению показывал только «что осталось». Свидетельство лежало в самом роадмапе healthlog: секция «Что уже пройдено» на двадцать строк прозы, руками, с припиской «Эти звенья целями не заведены: закрытая цель записи не оставляет». Теперь строка с датой переезжает в секцию достигнутого, файл удаляется по-прежнему. Вторым домом поведения это не делает: нормативное поведение живёт в openspec/specs, роадмап отвечает, когда и в каком порядке оно появилось. Ссылки на файл в строке нет — файла больше нет, форма как в REJECTED.md. Цель стала возможностью приложения, задача — шагом к ней: - заголовок цели отвечает на «что приложение будет уметь»; свойство поведения («сообщает о своём состоянии», «исход не зависит от порядка») — тоже возможность и переформулировки не требует; - «Завершение» — списком, а не абзацем: задача ссылается на его строку, и это новая защита от «отрефакторить X» вместо прежнего «наблюдаемо снаружи». Заодно видно обратное: строка, к которой не относится ни одна задача, — незакрытая часть возможности; - работа над инструментом и процессом на этот вопрос не отвечает и живёт в отдельной секции. Цель обязательна не у всякой задачи. Прежнее «иначе она не попадёт ни в один спринт» было угрозой, а не аргументом, и заставляло операционную работу выдумывать себе направление. Граница по роду: feature без цели не бывает, fix, chore и research живут без неё и входят в набор помимо цели спринта. Тип [epic] упразднён: зонтиком стала цель, а слишком крупный шаг дробится под ней. Ноль употреблений на 97 записей двух живых проектов. Секции роадмапа — умеет / строим / направления / станок, четыре вместо двух; имена приняты как временные и запаркованы (TODO 7). Имя секции достигнутого знает скрипт — docs/.pm.json, ключ tasks.achieved_section. reopen цели снимает строку достигнутого, круг проверен вживую. Всё дописано в версию 3 канона: она ещё нигде не выкачена. DECISIONS 19, YYY–ГГГ и следствия 78–81. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,12 +41,15 @@
|
||||
заведённое и уже выкинутое. Нашлось среди живых — дописываем находку в
|
||||
существующий файл. Нашлось в `REJECTED.md` — это сигнал: причина отказа могла
|
||||
устареть, выноси пользователю, а не заводи молча заново.
|
||||
4. **Разложи по целям.** У каждой заводимой задачи должен быть `goal:<слаг>`.
|
||||
Половина находок ревью не служит ничему из порядка — их цель **тематическая**
|
||||
(«прочность слияния», «журнал и пересборка», «наблюдаемость»).
|
||||
Подходящей темы нет — заведи её целью (`add --type goal --section темы`)
|
||||
в том же проходе: без цели задача не попадёт ни в один спринт, а значит не
|
||||
будет сделана никогда.
|
||||
4. **Разложи по целям — там, где цель нужна.** Большинство находок ревью это
|
||||
`fix` и `chore`, и **цель им не требуется**: они служат работоспособности, а
|
||||
не направлению, и в спринт входят помимо его цели. Придуманная им цель —
|
||||
ровно то враньё, от которого спасает род работы.
|
||||
|
||||
Цель обязательна у находки, которая оказалась **новой возможностью**
|
||||
(`kind:feature`): нашлось поведение, которого никто не заказывал, и его надо
|
||||
либо заказать целью, либо убрать. Подходящей цели нет — заведи её
|
||||
(`add --type goal --section направления`) в том же проходе.
|
||||
5. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в
|
||||
пакетный файл / уже заведено / отброшено, и под какую цель — пачкой через
|
||||
`AskUserQuestion`. Это тот же барьер, что и «три кандидата» в интейке из
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Декомпозиция и мозговой штурм
|
||||
|
||||
Обе операции превращают одну запись в несколько (или в ноль). Разница во входе:
|
||||
декомпозиция дробит **готовую задачу или эпик**, штурм прорабатывает **идею**,
|
||||
декомпозиция дробит **слишком крупную задачу**, штурм прорабатывает **идею**,
|
||||
которая ещё не задача.
|
||||
|
||||
## Тест декомпозиции
|
||||
@@ -11,10 +11,11 @@
|
||||
1. **Мерджатся независимо.** Часть Б не требует, чтобы часть А была уже влита.
|
||||
Есть порядок «сперва А, потом Б, иначе не собрать» → это не декомпозиция, а
|
||||
план реализации: шаги остаются **внутри одного файла**.
|
||||
2. **Каждая даёт видимую пользу.** Часть, полезная только в комплекте с другой,
|
||||
— не самостоятельная задача. Пользу проверяй тестом «готова к взятию»
|
||||
(task-format): что станет наблюдаемо иначе именно от этой части и какие у неё
|
||||
собственные критерии приёмки.
|
||||
2. **Каждая — самостоятельный шаг.** Часть, осмысленная только в комплекте с
|
||||
другой, — не задача. Проверяй тестом «готова к взятию» (task-format): какую
|
||||
строку «Завершения» цели двигает **именно эта часть** и какие у неё
|
||||
собственные критерии приёмки. У операционных частей (`fix`, `chore`,
|
||||
`research`) цели может не быть — тогда достаточно собственных критериев.
|
||||
|
||||
Не проходит хотя бы одно — **не дроби**. Ложная декомпозиция плодит файлы,
|
||||
которые нельзя взять поодиночке, и переоценка потом склеивает их обратно.
|
||||
@@ -55,21 +56,21 @@
|
||||
`REJECTED.md` здесь — не «выкинули», а именно тот след, что переживает запись:
|
||||
через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на
|
||||
наследников, а не археологией git;
|
||||
- родитель осмыслен как зонтик → `edit <slug> --type epic`, тело — ссылки на
|
||||
задачи-части, своих шагов у него нет. **Эпик не берётся в спринт** и живёт
|
||||
ровно до тех пор, пока не закрыта последняя часть.
|
||||
- родитель осмыслен как **возможность**, а не как шаг → это цель. Тип на месте
|
||||
не меняется (цель живёт в другом индексе): заводится `[goal]` в `ROADMAP.md`,
|
||||
части получают `--goal <новый слаг>`, родитель закрывается с причиной-ссылкой.
|
||||
|
||||
Зонтик, который перестал быть временным и описывает направление, а не работу, —
|
||||
это уже **цель**, а не эпик. Тип на месте не меняется (цель живёт в другом
|
||||
индексе): заводится `[goal]` в `ROADMAP.md`, задачи получают `--goal <новый слаг>`,
|
||||
эпик закрывается с причиной-ссылкой.
|
||||
**Промежуточного зонтика между целью и задачей нет.** Тип `[epic]` упразднён:
|
||||
роль зонтика играет цель, а слишком крупный шаг дробится на шаги помельче под
|
||||
той же целью. Если частям нужен общий заголовок — значит у них общая
|
||||
возможность, и её надо назвать целью, а не заводить временный тип.
|
||||
|
||||
## Когда декомпозиция случается посреди спринта
|
||||
|
||||
Задача, которая **переросла в эпик**, распознаётся до того, как под неё заведено
|
||||
предложение об изменении: иначе его придётся выбрасывать. Она помечается
|
||||
`[epic]`, выходит из набора (`sprint drop … --reason "переросла в эпик"`), уходит
|
||||
на декомпозицию, а спринт продолжается остальными. Части заводятся сразу, но в
|
||||
Задача, которая **оказалась крупнее задачи**, распознаётся до того, как под неё
|
||||
заведено предложение об изменении: иначе его придётся выбрасывать. Она выходит
|
||||
из набора (`sprint drop … --reason "крупнее задачи"`), уходит на декомпозицию, а
|
||||
спринт продолжается остальными. Части заводятся сразу под той же целью, но в
|
||||
текущий набор **не добавляются** — набор заморожен.
|
||||
|
||||
## Мозговой штурм идеи
|
||||
@@ -100,7 +101,7 @@ Applicative-штурм («перечисли задачи, следующие и
|
||||
|
||||
- Идея/задача на входе, выбранная рамка (для штурма), задачи-наследники со
|
||||
слагами, целями и секциями.
|
||||
- Судьба родителя: удалён / стал эпиком / стал целью / выкинут с причиной.
|
||||
- Судьба родителя: удалён / стал целью / выкинут с причиной.
|
||||
- `tasks.py check` после правок.
|
||||
- Границы покрытия: какие постановки рассмотрены и какие сознательно отброшены —
|
||||
чтобы штурм не пришлось повторять с нуля.
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
```
|
||||
|
||||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется
|
||||
префиксом `[goal]` / `[idea]` / `[epic]`; обычная задача — без префикса.
|
||||
префиксом `[goal]` / `[idea]`; обычная задача — без префикса.
|
||||
Отдельного поля типа **нет**: два места для одного факта разъезжаются, а
|
||||
префикс виден прямо в индексе, где и принимается решение «брать или не брать».
|
||||
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательна
|
||||
@@ -152,10 +152,14 @@
|
||||
|
||||
## Файл цели
|
||||
|
||||
```markdown
|
||||
# [goal] Прочность слияния
|
||||
**Заголовок цели отвечает на «что приложение будет уметь».** Не область работ и
|
||||
не имя подсистемы: не «Работа со слиянием», а «Исход слияния не зависит от
|
||||
порядка доставки». Свойство поведения — тоже возможность.
|
||||
|
||||
- **Секция:** темы
|
||||
```markdown
|
||||
# [goal] Исход слияния не зависит от порядка доставки
|
||||
|
||||
- **Секция:** направления
|
||||
- **Теги:** decomposed
|
||||
|
||||
Ради чего: точки из разных доставок сходятся в один часовой объект, и сегодня
|
||||
@@ -163,14 +167,18 @@
|
||||
|
||||
## Завершение
|
||||
|
||||
Достигнута, когда исход слияния не зависит ни от порядка, ни от времени
|
||||
доставки, и это подтверждено повторным прогоном на живом корпусе.
|
||||
- повторная доставка тех же точек в другом порядке даёт то же состояние;
|
||||
- накопительная метрика за сутки не уменьшается после повторной доставки;
|
||||
- в логе видно, какая из двух точек выиграла и почему.
|
||||
```
|
||||
|
||||
- **Задачи цели здесь не перечисляются.** Перечень даёт
|
||||
`tasks.py list --goal <слаг>`; хранимый список стал бы третьим индексом и
|
||||
поехал бы на первой же закрытой задаче.
|
||||
- **Раздел «Завершение»** — то, по чему видно, что цель достигнута.
|
||||
- **Раздел «Завершение» — списком, а не абзацем.** Это признаки того, что
|
||||
приложение уже умеет; **на строку «Завершения» ссылается задача**, объясняя,
|
||||
какую часть возможности она двигает (см. тест готовности). Абзацем такая
|
||||
ссылка не берётся, поэтому список.
|
||||
- **Тег `decomposed`** отличает «цель ещё не разобрана» от «все её задачи
|
||||
закрыты» — два состояния, у которых снаружи один и тот же признак: задач нет.
|
||||
Пометка именно **тегом**, а не строкой в теле: только так она проверяется.
|
||||
@@ -178,6 +186,12 @@
|
||||
законна и зелёного прогона не ломает; `check --fix` сам ставит его цели, у
|
||||
которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть.
|
||||
- Цель живёт в `ROADMAP.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`.
|
||||
- **Достигнутая цель не исчезает.** `close <слаг> --implemented` удаляет файл и
|
||||
переносит строку в секцию `умеет` с датой:
|
||||
`- 2026-08-04 \`merge-order\` — Исход слияния не зависит от порядка доставки. …`
|
||||
Ссылки на файл в ней нет — файл удалён, а битая ссылка это ошибка `check`.
|
||||
Поведение живёт в спеках проекта; роадмап отвечает, **когда и в каком порядке**
|
||||
оно появилось.
|
||||
|
||||
## Слаг
|
||||
|
||||
@@ -205,7 +219,7 @@
|
||||
|
||||
| Файл | Что отвечает | Секции |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md` | какие есть цели, в какой очереди идут и почему | порядок (очередь значима) и темы (порядка нет) |
|
||||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | `умеет` (достигнутое), `строим` (очередь значима), `направления` (очереди нет), `станок` (не про приложение) |
|
||||
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию ядро/инфра) |
|
||||
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
||||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||||
@@ -225,8 +239,11 @@
|
||||
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
|
||||
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
|
||||
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В секции
|
||||
**«порядок»** очередь значима и обосновывается прозой; двигают строку
|
||||
`move <slug> --section порядок --after <другой>`.
|
||||
**`строим`** очередь значима и обосновывается прозой; двигают строку
|
||||
`move <slug> --section строим --after <другой>`. В секции **`умеет`** строки
|
||||
не той формы, что у прочих индексов: дата, слаг, заголовок — как в
|
||||
`REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда). Имена
|
||||
секций временные, см. SKILL.md.
|
||||
|
||||
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
|
||||
Строку руками не пишут.
|
||||
@@ -265,11 +282,13 @@
|
||||
Единственный механизм разметки, потому что `list --tag` уже умеет отбирать по
|
||||
ним порцию разбора. Отдельных полей меты под это не заводим.
|
||||
|
||||
- `goal:<слаг>` — цель, которой служит задача. Обязателен: задача без цели не
|
||||
попадёт ни в один спринт.
|
||||
- `goal:<слаг>` — цель, которой служит задача. Обязателен **у `kind:feature`**:
|
||||
новая возможность и есть содержание цели. У `fix`, `chore` и `research` его
|
||||
может не быть — они служат работоспособности, а не направлению, и в набор
|
||||
спринта входят помимо его цели.
|
||||
- `kind:<род>` — род работы: `feature` | `fix` | `chore` | `research`. Словарь
|
||||
**закрыт**, значение ровно одно. Обязателен у задачи (без него `sprint take`
|
||||
откажет), у цели запрещён, у идеи и эпика необязателен. Ставится
|
||||
откажет), у цели запрещён, у идеи необязателен. Ставится
|
||||
`add --kind` / `edit --kind`; `--kind` заменяет прежнее значение, а не
|
||||
добавляет второе. Смысл рода и почему он тегом, а не префиксом — в SKILL.md,
|
||||
раздел «Род работы».
|
||||
@@ -303,15 +322,25 @@
|
||||
2. **Каких границ это касается** — раздел «Затрагивает». Без него задачу нельзя
|
||||
оценить: остаётся судить по длине текста.
|
||||
3. **По чему видно, что закончено** — критерии приёмки с оракулами.
|
||||
4. **Какой цели она служит** — тег `goal:` и одна строка «почему именно этой».
|
||||
4. **Какую часть «Завершения» своей цели она двигает** — у задачи с целью.
|
||||
Строкой: «двигает пункт 2 «Завершения» — накопительная метрика перестаёт
|
||||
уменьшаться». Это и есть защита от задачи «отрефакторить X»: она проваливает
|
||||
тест не потому, что невидима снаружи, а потому, что не находит строки, к
|
||||
которой относится. Заодно видно обратное — достаточен ли набор задач для
|
||||
цели: строка «Завершения», к которой не относится ни одна задача, это
|
||||
незакрытая часть возможности.
|
||||
|
||||
**У задачи без цели** (`fix`, `chore`, `research`) вопрос не задаётся: они
|
||||
служат работоспособности, а не направлению.
|
||||
|
||||
Не отвечается первый, второй или третий вопрос → это **идея** (`[idea]`), её
|
||||
место в штурме. Не отвечается четвёртый → либо цель есть и не проставлена, либо
|
||||
задача не служит ничему — тогда её не надо заводить.
|
||||
место в штурме. Не отвечается четвёртый у `feature` → либо цель есть и не
|
||||
проставлена, либо это не новая возможность.
|
||||
|
||||
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
|
||||
**эпик** (`[epic]`), сперва декомпозиция. Эпик временен и исчезает после
|
||||
разбора; цель (`[goal]`) постоянна — не путать.
|
||||
это **несколько задач под одной целью**, дроби сразу. Промежуточного зонтика
|
||||
между целью и задачей нет: тип `[epic]` упразднён, потому что зонтиком стала
|
||||
сама цель.
|
||||
|
||||
Тест применяется при заведении и при переоценке. К старым задачам, которых
|
||||
операция не касается, задним числом не применяется — беклог не переоформляют
|
||||
|
||||
Reference in New Issue
Block a user