роадмап — состояние проекта, а не очередь работ
Основной инструмент владельца отвечал на половину своего вопроса. Оценка идёт по поведению: что приложение уже может и чего ещё не может, — а 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:
+100
-38
@@ -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 …`, затем допиши тело редактором: одна фраза, **затрагиваемые границы**,
|
||||
|
||||
Reference in New Issue
Block a user