роадмап — состояние проекта, а не очередь работ

Основной инструмент владельца отвечал на половину своего вопроса. Оценка идёт
по поведению: что приложение уже может и чего ещё не может, — а 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:
av
2026-08-04 17:43:55 +03:00
co-authored by Claude Opus 5
parent 5bf599a767
commit e847bfa0ea
13 changed files with 453 additions and 136 deletions
+100 -38
View File
@@ -13,10 +13,17 @@ description: Ведение задач и целей как каталога mar
прошедшего спринта → переоценка → выбор цели и набор) — это скилл `session`; и
выполнением задачи — это пайплайн проекта.
## Четыре правила, из которых всё следует
## Пять правил, из которых всё следует
Ситуация не покрыта инструкцией — решай по ним.
0. **Цель — возможность приложения, задача — шаг к ней.** Цель отвечает на «что
приложение будет уметь», её «Завершение» — наблюдаемый признак того, что уже
умеет; задача отвечает на «что для этого нужно сделать». Оценивается проект
по **поведению**, а не по внутреннему устройству, поэтому и роадмап отвечает
не «сколько работ осталось», а «что уже умеет и чего ещё не умеет».
Свойство поведения — тоже возможность: «сообщает о своём состоянии»,
«исход слияния не зависит от порядка доставки» — законные цели.
1. **Беклог гниёт с той стороны, где его пополняют.** Заведение — самая частая
операция и с худшим отказом: из одного разговора рождается пять файлов, а
переоценка потом разгребает то, чего не надо было заводить. Дедупликация и
@@ -33,11 +40,13 @@ description: Ведение задач и целей как каталога mar
3. **Причина переживает запись.** Выкинутая без причины задача вернётся через
квартал тем же текстом. Реализованная оставляет след в коммите — выкинутая не
оставляет ничего, поэтому у неё есть `REJECTED.md`.
4. **Порядка нет, есть цель.** Ни в секциях, ни списком: «что делать дальше»
отвечает набор спринта, а между спринтами порядок не нужен никому — брать
задачи вне спринта запрещает заморозка. Поэтому нет ни приоритетов, ни
«повысить», ни «встать раньше»: вместо повышения — смена цели или включение
в набор.
4. **Порядка нет, есть цель — но цель есть не у всякой задачи.** Приоритетов,
«повысить» и «встать раньше» нет: «что делать дальше» отвечает набор спринта,
а между спринтами порядок не нужен никому. Цель обязательна там, где она и
есть содержание работы, — у **новой возможности** (`kind:feature`). Починка,
техдолг и разведка служат работоспособности, а не направлению, и живут без
цели законно; в набор спринта они входят помимо его цели. Придуманная им цель
— то же враньё, от которого спасает род работы.
## Раскладка
@@ -48,7 +57,7 @@ description: Ведение задач и целей как каталога mar
```
docs/tasks/
items/ задачи и цели файлами, <slug>.md, слаги английские
ROADMAP.md оглавление целей: порядок (значим) и темы (без порядка)
ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет
BACKLOG.md что можно взять — только задачи, целей здесь нет
SPRINT.md текущий спринт: цель, набор, дата
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
@@ -58,6 +67,20 @@ docs/tasks/
подо что берут.** Цель в спринт взять нельзя, поэтому в списке берущихся ей не
место.
**Четыре секции роадмапа, и первая отвечает на половину вопроса:**
| Секция | Что в ней |
| --- | --- |
| `умеет` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках |
| `строим` | очередь значима и обосновывается прозой рядом |
| `направления` | очереди нет, тянутся долго |
| `станок` | инструмент и процесс разработки — не возможности приложения, и потому отдельно |
Имена секций сейчас **временные**: они читаются хуже, чем должны, и будут
пересмотрены (`DECISIONS.md`, тема 19). Проект вправе назвать свои иначе —
секции берутся из заголовков индекса, — но дом достигнутого скрипт обязан знать
по имени, и оно живёт в `docs/.pm.json`, ключ `tasks.achieved_section`.
**Секции «блокеры» в беклоге нет.** Блокер — это *состояние* (спринт не может
продолжаться ни одной задачей), а не полка: он живёт ровно до ответа человека, и
записи в такой секции не успевают жить. Следы блокера остаются вопросами в
@@ -77,6 +100,15 @@ docs/tasks/
даром: `SPRINT.md` лежит под git, `git log -p docs/tasks/SPRINT.md` отдаёт историю
всех наборов без отдельного журнала.
**У достигнутой цели запись остаётся, и это единственное исключение.** Файл
удаляется так же, а строка переезжает в секцию `умеет` с датой. Причина в том,
что цель — не работа, а **возможность**: «что приложение умеет» это половина
вопроса, ради которого роадмап и открывают, и стирать её вместе с файлом значит
оставить инструмент, отвечающий только «что осталось». Вторым домом это не
становится: поведение живёт в `openspec/specs/`, а роадмап отвечает **когда и в
каком порядке оно появилось** — другой вопрос. Ссылки на файл в строке нет
намеренно: файл удалён, а битая ссылка — законная ошибка `check`.
Куда запись может переехать и какой командой — весь набор переходов:
```mermaid
@@ -86,6 +118,7 @@ stateDiagram-v2
state "SPRINT.md — набор спринта" as S
state "REJECTED.md — ушла без реализации" as R
state "записи нет — реализована" as D
state "ROADMAP.md, «умеет» — цель достигнута" as A
[*] --> B: add
[*] --> P: add --type goal
@@ -94,10 +127,13 @@ stateDiagram-v2
B --> S: sprint take
S --> B: sprint drop --reason
S --> D: close --implemented
P --> A: close --implemented
B --> R: close --reason
S --> R: close --reason
P --> R: close --reason
D --> B: reopen --reason
R --> B: reopen --reason
A --> P: reopen --reason
```
Состояния здесь — **где числится строка**, а не где лежит файл: файл
@@ -110,36 +146,55 @@ stateDiagram-v2
## Цели
**Цель — такой же файл в `items/`, тип `[goal]`**, перечисленный в `ROADMAP.md`:
либо цель из секции **порядок** — там очередь значима и обоснована прозой, — либо
**тематическая**, в порядок не встающая («прочность слияния», «журнал и
пересборка»). Без второй секции половина целей была бы нигде не перечислена:
находки ревью не служат ничему из порядка.
**Цель — возможность приложения.** Такой же файл в `items/`, тип `[goal]`,
перечисленный в `ROADMAP.md`. Формулируется ответом на вопрос **«что приложение
будет уметь»**, а не названием области работ: не «Работа с чтением», а «Чтение
данных клиентами»; не «Рефакторинг слияния», а «Исход слияния не зависит от
порядка доставки».
**Свойство поведения — тоже возможность.** «Наблюдаемость» это «приложение
сообщает о своём состоянии»; «прочность слияния» это «исход не зависит от
порядка». Такие цели законны и переформулировки в функцию не требуют — требуют
только, чтобы формулировка отвечала на «что приложение делает», а не на «какую
часть кода мы трогаем».
**Что целью не является — работа над станком.** Инструмент, процесс, сборка,
сам этот скилл: на вопрос «что приложение будет уметь» они не отвечают. Им
отведена отдельная секция роадмапа, чтобы они были видны в том же экране и при
этом не читались как возможности продукта.
Секция выбирается так: очередь значима и обоснована прозой — `строим`; тянется
долго и очереди не имеет — `направления`; не про приложение — `станок`;
достигнутое кладёт туда сам `close`.
- **Список задач цели выводится, а не хранится.** В теле цели — зачем она и что
считается её завершением; перечня задач там нет. Он был бы третьим индексом и
поехал бы на первой же закрытой задаче, а `check` про него не знает. Связь
однонаправленна: задача несёт тег `goal:<слаг>`, перечень даёт
`tasks.py list --goal <слаг>`.
- **Статус цели выводится.** Цель закрыта, когда у неё не осталось открытых
- **Статус цели выводится.** Цель достигнута, когда у неё не осталось открытых
задач; `[x]`/`[~]` руками не ведутся, а `close` цели с живыми задачами
скрипт запретит. Единственная оговорка: цель без задач неотличима — «ещё не
скрипт запретит. Достижение — `close <цель> --implemented`: файл удаляется,
строка с датой переезжает в `умеет`. Ошиблись — `reopen` вернёт файл и
**снимет строку достигнутого**, чтобы роадмап не утверждал того, чего нет. Единственная оговорка: цель без задач неотличима — «ещё не
разобрана» или «всё закрыто». Различает **тег `decomposed`** в мете
цели: он ставится, когда цель разложена на задачи. Тег, а не строка в теле —
потому что проверяется механически: `check` **напоминает** о нём у пустой цели
(замечанием, не ошибкой — неразобранная цель это законное состояние), а `check
--fix` сам проставляет его цели, у которой задачи есть.
- **`[goal]` и `[epic]` разные вещи.** Цель **постоянна**: живёт, пока живёт
направление. Эпик **временен**: это задача, которая не мерджится целиком, её
разбирают, и он исчезает. Два срока жизни под одним словом разъезжаются,
поэтому слова два.
- **Тип `[epic]` упразднён.** Он был зонтиком между целью и задачами — «задача,
которая не мерджится целиком». Зонтик теперь цель, а слишком крупный шаг просто
дробится на шаги помельче под той же целью, и промежуточному типу места не
осталось. Замер подтвердил: ноль употреблений на 97 записей двух живых
проектов. Встретился в чужом беклоге — это цель либо набор задач, и `check`
назовёт его неизвестным типом.
## Род работы
**Тип записи и род работы — две оси, и путать их нельзя.** Тип отвечает «что это
за запись» (цель, идея, эпик, задача), род — «какого рода работа»: `feature`,
`fix`, `chore`, `research`. Одним значением на оба вопроса не ответить: идея
бывает *про* функцию, эпик функцией *и является*.
за запись» (цель, идея, задача), род — «какого рода работа»: `feature`, `fix`,
`chore`, `research`. Одним значением на оба вопроса не ответить: идея бывает
*про* функцию, а цель функцией *и является*.
- **`feature`** — снаружи появляется или меняется то, чего раньше не было.
- **`fix`** — поведение расходится с заявленным, и расхождение воспроизводится.
@@ -163,11 +218,18 @@ stateDiagram-v2
`defect`, — и отбор по роду перестанет отвечать на свой единственный вопрос. Ни
один род не подходит — это сигнал, что в задаче их два и её надо разделить.
**Род обязателен у задачи, у цели запрещён, у идеи и эпика необязателен** — идея
получает его, когда становится задачей, а эпик исчезает после разбора, и род
несут его части. Требуется он там, где по нему принимают решение: `sprint take`
без рода откажет. `check` о пропаже только **напоминает** — беклог, заведённый до
появления рода, законен, и переоформлять его «заодно» здесь не просят.
**Род обязателен у задачи, у цели запрещён, у идеи необязателен** — идея получает
его, когда становится задачей. Требуется он там, где по нему принимают решение:
`sprint take` без рода откажет. `check` о пропаже только **напоминает** — беклог,
заведённый до появления рода, законен, и переоформлять его «заодно» здесь не
просят.
**Род решает и то, обязательна ли цель.** `feature` без цели не бывает: новая
возможность и есть содержание цели, и если подходящей нет — либо она заводится,
либо это не `feature`. `fix`, `chore` и `research` живут без цели законно, и
`check` о них молчит: они служат работоспособности, а не направлению. Это
единственный случай, когда род что-то определяет за пределами отбора, — и
определяет он учёт, а не процесс проверки.
**Род не выбирает профиль ревью и вообще ничего не предписывает пайплайну.**
Профиль выбирается по факту изменения, а не по роду задачи: `chore` бывает
@@ -216,7 +278,7 @@ stateDiagram-v2
python3 $tk check --dir D # согласованность индексов + здоровье
python3 $tk check --dir D --fix # + починить дрейф (секция, заголовок, дубли, «зачем», форма меты)
python3 $tk list --dir D [--stale] [--section S] [--type T] [--kind K] [--tag a,b] [--goal S] [--index …] [--questions]
python3 $tk add --dir D --slug S --title T [--type goal|idea|epic] [--section S] [--goal G] [--kind K] [--why «зачем»] [--tag a,b]
python3 $tk add --dir D --slug S --title T [--type goal|idea] [--section S] [--goal G] [--kind K] [--why «зачем»] [--tag a,b]
python3 $tk edit S --dir D [--title T] [--why «зачем»] [--type T] [--goal G] [--kind K] [--add-tag a,b] [--rm-tag c]
python3 $tk move S --dir D --section S [--reason R] [--after S | --first]
python3 $tk close S --dir D --reason R # в REJECTED.md + удалить (ушла без реализации)
@@ -240,9 +302,9 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
нет» — нерабочая, и одинаковая реакция на них была бы неверна в обоих случаях.
Тип — английское ключевое слово `goal` / `idea` / `epic` / `task` (как и прочие
токены команд); `task` префикса не несёт, остальные кодируются `[goal]`/
`[idea]`/`[epic]` в заголовке. Текст задачи при этом русский.
Тип — английское ключевое слово `goal` / `idea` / `task` (как и прочие токены
команд); `task` префикса не несёт, остальные кодируются `[goal]`/`[idea]` в
заголовке. Текст задачи при этом русский.
**Мутации правят файл и индексы заодно** — руками строку индекса или мету
не пиши, зови `add`/`edit`/`move`/`close`/`sprint`. Смена заголовка, «зачем», типа,
@@ -310,14 +372,14 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
молча заводить нельзя). Две задачи об одном — самая дорогая находка
переоценки.
3. **Тип по тесту готовности** (см. task-format): проходит — задача, не
проходит — идея (`--type idea`), проходит по пользе, но не делается одним
заходом — эпик (`--type epic`, сперва декомпозиция). Направление, а не
работа — цель (`--type goal`).
4. **Цель задачи.** У каждой задачи должен быть `--goal <слаг>`: задача вне цели
не попадёт ни в один спринт. Подходящей цели нет — либо она заводится
(`--type goal` в «темы»), либо это сигнал, что задача никому не служит и
заводить её не надо. У идеи цели может не быть — она проставляется, когда
идея становится задачей.
проходит — идея (`--type idea`). Не делается одним заходом — это не эпик, а
несколько задач под одной целью: дроби сразу. Возможность приложения, а не
шаг — цель (`--type goal`).
4. **Цель задачи — если род её требует.** У `feature` должен быть
`--goal <слаг>`: новая возможность и есть содержание цели. Подходящей нет —
либо она заводится (`--type goal`), либо перед тобой не `feature`. У `fix`,
`chore` и `research` цели может не быть вовсе, и придумывать её не надо. У
идеи цель проставляется, когда идея становится задачей.
5. **Род работы**`--kind feature|fix|chore|research` (см. «Род работы»). Не
подходит ни один — задача не одна, разбирай.
6. `add …`, затем допиши тело редактором: одна фраза, **затрагиваемые границы**,