роадмап — состояние проекта, а не очередь работ
Основной инструмент владельца отвечал на половину своего вопроса. Оценка идёт по поведению: что приложение уже может и чего ещё не может, — а 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 …`, затем допиши тело редактором: одна фраза, **затрагиваемые границы**,
|
||||
|
||||
@@ -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]` упразднён, потому что зонтиком стала
|
||||
сама цель.
|
||||
|
||||
Тест применяется при заведении и при переоценке. К старым задачам, которых
|
||||
операция не касается, задним числом не применяется — беклог не переоформляют
|
||||
|
||||
@@ -12,7 +12,7 @@ av-dev, и подгоняется под него проект. Имена вн
|
||||
|
||||
docs/tasks/
|
||||
items/ задачи и цели файлами, <slug>.md
|
||||
ROADMAP.md оглавление целей: порядок (значим) и темы (без порядка)
|
||||
ROADMAP.md состояние проекта: что уже умеет и чего ещё не умеет
|
||||
BACKLOG.md что можно взять — только задачи, целей здесь нет
|
||||
SPRINT.md текущий спринт: цель, набор, дата, слаг
|
||||
REJECTED.md ушедшее БЕЗ реализации, с причиной и датой
|
||||
@@ -24,13 +24,20 @@ av-dev, и подгоняется под него проект. Имена вн
|
||||
задача — знают индексы**, потому что «в спринте» это свойство спринта, а не
|
||||
задачи; поля-состояния в файле нет, а рассогласование ловит check.
|
||||
|
||||
Тип — ключевое слово (goal | idea | epic | task), по-английски, как и прочие
|
||||
токены команд. Обычная задача (task) префикса не несёт, остальные кодируются
|
||||
префиксом `[goal]`/`[idea]`/`[epic]` в заголовке H1. Отдельного поля типа нет.
|
||||
Тип — ключевое слово (goal | idea | task), по-английски, как и прочие токены
|
||||
команд. Обычная задача (task) префикса не несёт, остальные кодируются префиксом
|
||||
`[goal]`/`[idea]` в заголовке H1. Отдельного поля типа нет.
|
||||
|
||||
`[goal]` и `[epic]` — разные вещи: цель постоянна и живёт, пока живёт
|
||||
направление; эпик временен — это задача, которая не мерджится целиком, её
|
||||
разбирают, и он исчезает.
|
||||
**Цель — возможность приложения**, а не тема работ: она отвечает на вопрос «что
|
||||
приложение будет уметь», её «Завершение» — наблюдаемый признак того, что уже
|
||||
умеет. Задача — шаг к этой возможности, она отвечает на «что для этого нужно
|
||||
сделать». Отсюда роадмап и есть состояние проекта: достигнутая цель не
|
||||
исчезает, а переезжает строкой с датой в секцию достигнутого.
|
||||
|
||||
**Цель обязательна не у всякой задачи.** Новая возможность (`kind:feature`) без
|
||||
цели не бывает — цель и есть её содержание. Починка, техдолг и разведка служат
|
||||
работоспособности, а не направлению, и живут без цели законно; в набор спринта
|
||||
они входят помимо его цели.
|
||||
|
||||
**Род работы — вторая ось, и она отвечает на другой вопрос.** Тип записи говорит,
|
||||
что это за запись; род (`feature` | `fix` | `chore` | `research`, тегом
|
||||
@@ -44,7 +51,7 @@ av-dev, и подгоняется под него проект. Имена вн
|
||||
tasks.py list [--dir DIR] [--stale] [--section S] [--type T] [--tag a,b]
|
||||
[--goal S] [--kind K] [--index backlog|sprint|roadmap|all]
|
||||
[--questions]
|
||||
tasks.py add --slug S --title T [--type goal|idea|epic] [--section S]
|
||||
tasks.py add --slug S --title T [--type goal|idea] [--section S]
|
||||
[--goal G] [--kind K] [--why H] [--reason R] [--tag a,b] [--dir DIR]
|
||||
tasks.py edit S [--title T] [--why H] [--type T] [--goal G] [--kind K]
|
||||
[--add-tag a,b] [--rm-tag c,d] [--section S] [--dir DIR]
|
||||
@@ -107,6 +114,9 @@ DEFAULTS = {
|
||||
"sprint": "SPRINT.md",
|
||||
"rejected": "REJECTED.md",
|
||||
"sprint_section": "Набор",
|
||||
# Секция роадмапа, куда переезжает достигнутая цель. Скрипт обязан знать её
|
||||
# имя: остальные секции он берёт из заголовков как есть, а в эту пишет сам.
|
||||
"achieved_section": "умеет",
|
||||
"criteria_heading": "Критерии приёмки",
|
||||
"surface_heading": "Затрагивает",
|
||||
"questions_heading": "Вопросы",
|
||||
@@ -118,7 +128,7 @@ DEFAULTS = {
|
||||
PATH_KEYS = ("items", "backlog", "roadmap", "sprint", "rejected")
|
||||
|
||||
DEFAULT_SECTIONS = "ядро,инфра"
|
||||
DEFAULT_ROADMAP_SECTIONS = "порядок,темы"
|
||||
DEFAULT_ROADMAP_SECTIONS = "умеет,строим,направления,станок"
|
||||
|
||||
# Мета — список под заголовком, поле на строку. Старая форма (все поля одной
|
||||
# строкой через `·`) читается по-прежнему: у проектов на диске лежат файлы в
|
||||
@@ -179,7 +189,7 @@ BULLET = re.compile(r"^[-*]\s+(.*)$")
|
||||
REJECTED_ENTRY = re.compile(r"^- \d{4}-\d{2}-\d{2} `[a-z0-9-]+` — .+")
|
||||
|
||||
GOAL = "goal"
|
||||
TYPES = ("goal", "idea", "epic") # непустые типы-ключевые слова, префикс [..] в H1
|
||||
TYPES = ("goal", "idea") # непустые типы-ключевые слова, префикс [..] в H1
|
||||
PLAIN_TYPE = "task" # обычная задача — без префикса
|
||||
TAKEABLE = (PLAIN_TYPE,) # что вообще можно взять в спринт
|
||||
QUESTION_TAG = "question"
|
||||
@@ -192,6 +202,10 @@ SPRINT_TAG = "sprint:"
|
||||
# есть единственный механизм разметки, а `list --tag` уже умеет отбирать.
|
||||
KIND_TAG = "kind:"
|
||||
KINDS = ("feature", "fix", "chore", "research")
|
||||
# Цель обязательна только у новой возможности: цель и есть возможность.
|
||||
# Починка, техдолг и разведка служат работоспособности, а не направлению —
|
||||
# придуманная им цель это то же враньё, от которого спасает род работы.
|
||||
NEEDS_GOAL = ("feature",)
|
||||
DECOMPOSED_TAG = "decomposed" # цель разложена на задачи (см. «Статус цели»)
|
||||
STALE_DAYS = 180 # порог «залежалась» для метрики здоровья в check
|
||||
CRITERIA_MIN, CRITERIA_MAX = 2, 5 # сколько утверждений в критериях приёмки
|
||||
@@ -841,9 +855,13 @@ def check(lay: Layout, fix: bool = False) -> int:
|
||||
if task["type"] == "idea":
|
||||
notes.append(f"{name}: идея без цели — цель проставляется,"
|
||||
f" когда идея становится задачей")
|
||||
else:
|
||||
errors.append(f"{name}: нет тега {GOAL_TAG}<слаг> — задача вне цели"
|
||||
f" не попадёт ни в один спринт")
|
||||
elif task["kind"] in NEEDS_GOAL:
|
||||
errors.append(f"{name}: род «{task['kind']}» без тега"
|
||||
f" {GOAL_TAG}<слаг> — новая возможность и есть"
|
||||
f" содержание цели. Либо цель заводится, либо"
|
||||
f" это не {task['kind']}")
|
||||
# Операционная задача (fix, chore, research) живёт без цели
|
||||
# законно: она служит работоспособности, а не направлению.
|
||||
elif task["goal"] not in goal_slugs:
|
||||
errors.append(f"{name}: тег {GOAL_TAG}{task['goal']} указывает на цель,"
|
||||
f" которой нет в {lay.cfg['items']}/")
|
||||
@@ -1053,7 +1071,7 @@ def list_tasks(lay: Layout, a: argparse.Namespace) -> int:
|
||||
kind = "" if a.kind else f"{t['kind']:<9}"
|
||||
goal = f" →{t['goal']}" if t["goal"] and not a.goal else ""
|
||||
flag = " ?" if questions_open(lay, t) else " "
|
||||
print(f"{touched}{t['place']:<8}{t['section']:<8}{kind}{flag} "
|
||||
print(f"{touched}{t['place']:<8}{t['section']:<12}{kind}{flag} "
|
||||
f"{t['path'].stem:<44} {rtype}{t['bare']}{goal}")
|
||||
print(f"\nвсего: {len(rows)}")
|
||||
|
||||
@@ -1253,9 +1271,10 @@ def cmd_add(lay: Layout, a: argparse.Namespace) -> int:
|
||||
elif not rtype:
|
||||
print(f" без рода работы — проставь `tasks.py edit {a.slug} --kind"
|
||||
f" {'|'.join(KINDS)}`: в спринт без него не возьмут")
|
||||
if rtype != GOAL and not any(t.startswith(GOAL_TAG) for t in tags):
|
||||
print(" без цели: задача вне цели не попадёт ни в один спринт —"
|
||||
f" проставь `tasks.py edit {a.slug} --goal <слаг>`")
|
||||
if (rtype != GOAL and (a.kind or "") in NEEDS_GOAL
|
||||
and not any(t.startswith(GOAL_TAG) for t in tags)):
|
||||
print(f" род «{a.kind}» без цели: новая возможность и есть содержание"
|
||||
f" цели — проставь `tasks.py edit {a.slug} --goal <слаг>`")
|
||||
# Урожай спринта метится сам: тег, который никто не ставит, не отбирает
|
||||
# первую порцию переоценки, а именно на ней держится правило «сперва урожай».
|
||||
sslug = sprint_slug(lay)
|
||||
@@ -1455,6 +1474,23 @@ def cmd_move(lay: Layout, a: argparse.Namespace) -> int:
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
def is_achieved(task: dict, a: argparse.Namespace) -> bool:
|
||||
"""Цель, закрытая как достигнутая: `close --implemented` без причины."""
|
||||
return task["type"] == GOAL and not a.reason
|
||||
|
||||
|
||||
def achieved_entry(task: dict, slug: str, date: str) -> str:
|
||||
"""Строка секции достигнутого. Ссылки на файл в ней нет намеренно: файл
|
||||
удаляется, а битую ссылку `check` справедливо назовёт ошибкой. Поведение
|
||||
живёт в спеках проекта — здесь остаётся «что и когда стало возможно»."""
|
||||
why = task["why"]
|
||||
tail = f" {why[0].upper()}{why[1:]}" if why else ""
|
||||
if tail and not tail.rstrip().endswith((".", "!", "?")):
|
||||
tail = tail.rstrip() + "."
|
||||
dot = "" if task["bare"].endswith((".", "!", "?")) else "."
|
||||
return f"- {date} `{slug}` — {task['bare']}{dot}{tail}"
|
||||
|
||||
|
||||
def cmd_close(lay: Layout, a: argparse.Namespace) -> int:
|
||||
for err in (bad_slug(a.slug), bad_reason(a.reason)):
|
||||
if err:
|
||||
@@ -1474,24 +1510,40 @@ def cmd_close(lay: Layout, a: argparse.Namespace) -> int:
|
||||
f" Цель закрыта, когда не осталось её задач")
|
||||
|
||||
plan = Plan()
|
||||
today = datetime.date.today().isoformat()
|
||||
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}"
|
||||
bullet = (f"- {today} `{a.slug}` — {task['title']}. Причина: {reason}{dot}"
|
||||
f" Была секция: {task['section'] or '—'}.")
|
||||
rej = lay.index("rejected")
|
||||
prev = rej.read_text(encoding="utf-8") if rej.exists() else "# Ушедшее без реализации\n"
|
||||
if not prev.endswith("\n"):
|
||||
prev += "\n"
|
||||
plan.file(rej, prev + bullet + "\n")
|
||||
# Достигнутая цель — единственная запись, переживающая удаление файла без
|
||||
# причины. Роадмап отвечает не только «что осталось», но и «что уже умеет»,
|
||||
# а этот ответ иначе стирался вместе с целью, и его вели прозой руками.
|
||||
achieved = achieved_entry(task, a.slug, today) if is_achieved(task, a) else None
|
||||
for kind_index, (lines, ei) in places.items():
|
||||
lines.pop(ei)
|
||||
if achieved is not None and kind_index == "roadmap":
|
||||
insert_entry(lines, lay.cfg["achieved_section"], achieved, first=True)
|
||||
achieved = None
|
||||
plan.index(lay, kind_index, lines)
|
||||
if achieved is not None: # строки в роадмапе не было — чиним
|
||||
lines = read_lines(lay.index("roadmap"))
|
||||
insert_entry(lines, lay.cfg["achieved_section"], achieved, first=True)
|
||||
plan.index(lay, "roadmap", lines)
|
||||
plan.delete(path)
|
||||
plan.commit()
|
||||
|
||||
print(f"{a.slug}: {'записано в ' + lay.name('rejected') + ' + удалено' if a.reason else 'удалено (реализовано, есть коммит)'}")
|
||||
if is_achieved(task, a):
|
||||
print(f"{a.slug}: цель достигнута — строка перенесена в"
|
||||
f" {lay.name('roadmap')}, секция «{lay.cfg['achieved_section']}»,"
|
||||
f" файл удалён")
|
||||
else:
|
||||
print(f"{a.slug}: {'записано в ' + lay.name('rejected') + ' + удалено' if a.reason else 'удалено (реализовано, есть коммит)'}")
|
||||
if "sprint" in places and a.reason:
|
||||
print(" задача закрыта прямо из спринта без реализации — назови это в докладе спринта")
|
||||
if not a.reason:
|
||||
@@ -1563,6 +1615,17 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int:
|
||||
if target == "backlog" and goal_of_sprint and tmp["goal"] == goal_of_sprint:
|
||||
target, section = "sprint", lay.cfg["sprint_section"]
|
||||
lines = read_lines(lay.index(target))
|
||||
# Строка достигнутого снимается ДО вставки и на том же списке: иначе вторая
|
||||
# правка читает индекс с диска, где первой ещё нет, и затирает её.
|
||||
unachieved: list[str] = []
|
||||
if kind == GOAL:
|
||||
kept = []
|
||||
for line in lines:
|
||||
if REJECTED_ENTRY.match(line) and f"`{a.slug}`" in line:
|
||||
unachieved.append(line)
|
||||
else:
|
||||
kept.append(line)
|
||||
lines = kept
|
||||
if find_entry_index(lines, a.slug) is None:
|
||||
hi, sec = find_section(lines, section)
|
||||
if hi is None:
|
||||
@@ -1570,6 +1633,8 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int:
|
||||
raise Usage(f"секции «{section}» нет в {lay.name(target)} (есть: {avail})")
|
||||
insert_entry(lines, sec, entry_line(lay, title, a.slug, tmp["why"]))
|
||||
plan.index(lay, target, lines)
|
||||
elif unachieved:
|
||||
plan.index(lay, target, lines)
|
||||
|
||||
rej = lay.index("rejected")
|
||||
if rej.is_file():
|
||||
@@ -1589,6 +1654,10 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int:
|
||||
+ (f" (секция «{section}»)" if target != "sprint" else " (набор спринта)"))
|
||||
for line in removed:
|
||||
print(f" снята строка {lay.name('rejected')}: {line.strip()}")
|
||||
for line in unachieved:
|
||||
print(f" снята строка «{lay.cfg['achieved_section']}» в"
|
||||
f" {lay.name('roadmap')}: {line.strip()}")
|
||||
print(" роадмап больше не утверждает, что приложение это умеет")
|
||||
print(" сверь тело: оно восстановлено на момент удаления, всё позднейшее"
|
||||
" живёт только в коммите задачи")
|
||||
return EXIT_OK
|
||||
@@ -1673,10 +1742,14 @@ def cmd_sprint_take(lay: Layout, a: argparse.Namespace) -> int:
|
||||
t = parse_task(path)
|
||||
if t["type"] not in TAKEABLE:
|
||||
raise Usage(f"{slug}: тип «{t['type']}» в спринт не берётся —"
|
||||
f" идея идёт на штурм, эпик на декомпозицию, цель не берут вовсе")
|
||||
if t["goal"] != goal_slug:
|
||||
raise Usage(f"{slug}: цель «{t['goal'] or '—'}» не цель спринта «{goal_slug}» —"
|
||||
f" идея идёт на штурм, цель не берут вовсе")
|
||||
if t["goal"] and t["goal"] != goal_slug:
|
||||
raise Usage(f"{slug}: цель «{t['goal']}» не цель спринта «{goal_slug}» —"
|
||||
f" набор служит одной цели, даже если взять удобно")
|
||||
if not t["goal"] and t["kind"] in NEEDS_GOAL:
|
||||
raise Usage(f"{slug}: род «{t['kind']}» без цели —"
|
||||
f" новая возможность и есть содержание цели"
|
||||
f" (`edit {slug} --goal <слаг>`)")
|
||||
# Отказ по факту, а не по метке: непустой раздел «Вопросы» блокирует
|
||||
# взятие независимо от тега. Забывший тег иначе проходил бы, а
|
||||
# поставивший спотыкался — стимул ровно обратный правилу.
|
||||
@@ -1987,10 +2060,17 @@ def init_files(lay: Layout, sections: list[str], roadmap_sections: list[str],
|
||||
+ "".join(f"## {s}\n\n" for s in sections))
|
||||
out[lay.index("roadmap")] = (
|
||||
"# Роадмап\n\n"
|
||||
f"Оглавление целей. Цель — файл `[goal]` в `{lay.cfg['items']}/`; её задачи\n"
|
||||
"здесь **не перечисляются** — перечень даёт `tasks.py list --goal <слаг>`.\n"
|
||||
f"В первой секции («{roadmap_sections[0]}») очередь значима и обосновывается\n"
|
||||
"прозой; в остальных порядка нет — это тематические цели.\n\n"
|
||||
"Состояние проекта: что приложение **уже умеет** и чего ещё не умеет.\n"
|
||||
f"Цель — возможность приложения, файл `[goal]` в `{lay.cfg['items']}/`; её\n"
|
||||
"задачи здесь **не перечисляются** — перечень даёт\n"
|
||||
"`tasks.py list --goal <слаг>`.\n\n"
|
||||
f"- **{lay.cfg['achieved_section']}** — достигнутое: строку пишет\n"
|
||||
" `tasks.py close <цель> --implemented`, ссылки на файл в ней нет —\n"
|
||||
" файл удаляется, поведение живёт в спеках;\n"
|
||||
"- **строим** — очередь значима и обосновывается прозой;\n"
|
||||
"- **направления** — очереди нет, тянутся долго;\n"
|
||||
"- **станок** — инструмент и процесс разработки, не возможности\n"
|
||||
" приложения. Отдельно, чтобы не смешиваться с ними.\n\n"
|
||||
+ "".join(f"## {s}\n\n" for s in roadmap_sections))
|
||||
out[lay.index("sprint")] = empty_sprint(lay)
|
||||
out[lay.index("rejected")] = (
|
||||
@@ -2180,6 +2260,17 @@ def scan_list_file(path: Path) -> dict:
|
||||
return found
|
||||
|
||||
|
||||
def first_open_section(raw: str) -> str:
|
||||
"""Секция, куда adopt кладёт выведенные цели: первая **не** достигнутая.
|
||||
|
||||
Первой в роадмапе идёт секция достигнутого, и класть в неё цель, выведенную
|
||||
из шага плана, значит объявить сделанным то, что ещё не начато.
|
||||
"""
|
||||
done = DEFAULTS["achieved_section"].lower()
|
||||
names = [s.strip() for s in raw.split(",") if s.strip()]
|
||||
return next((s for s in names if s.lower() != done), "строим")
|
||||
|
||||
|
||||
def cmd_adopt_scan(a: argparse.Namespace) -> int:
|
||||
sources = [Path(s) for s in a.sources]
|
||||
for s in sources:
|
||||
@@ -2195,7 +2286,7 @@ def cmd_adopt_scan(a: argparse.Namespace) -> int:
|
||||
rejected += sc.get("rejected", [])
|
||||
unclassified += sc.get("unclassified", [])
|
||||
goals += [{"slug": "", "title": g["title"],
|
||||
"section": (a.roadmap_sections.split(",")[0].strip() or "порядок"),
|
||||
"section": first_open_section(a.roadmap_sections),
|
||||
"from": g["from"], "step": g.get("step"), "done": g.get("done"),
|
||||
"body": f"Выведена из шага «{g['title']}» ({g['from']})."
|
||||
+ ("\n\nШаг помечен закрытым — цель, скорее всего,"
|
||||
|
||||
Reference in New Issue
Block a user