Команда stage была дефектна по шести пунктам, и все шесть подтверждены прогоном: не звала raw_last (переход оставлял каталог красным), не переписывала шапку беклога (индекс продолжал объявлять прежнюю стадию), шла в обход write_config, молча пропускала файлы с непересобираемой метой, ломалась на беклоге без заголовков и схлопывала полки при первом объявлении стадии. Объявление и смена разведены: объявление беклога не трогает вовсе, смена трогает состав секций только по явному --sections, а слить полки скрипт не берётся ни в одном случае. Абзац шапки размечен парой «стадия», и расхождение с конфигом стало обычным дрейфом. Отказ по недостающей строке индекса запирал запись, пережившую упразднение роадмапа: edit, close и reopen теперь заводят или пропускают строку сами. Прочее: регистр stage нормализуется при чтении; --fix снимает мёртвые теги и у неразобранных записей; move отказывает переставлять сырьё; adopt держит место сырья; docs.py bump двигает одну запись журнала за раз; tasks.py получил перечень упразднённых адресов, и гейт наконец видит собственное упразднение ROADMAP.md. Запись «Версия 3» переписана по прогону на игрушечном проекте: прежний порядок шагов был неисполним. Закрыты дыры модели стадий (пересмотр плана стройки стал сценарием, приёмка отвязана от груминга, from-review, research и adopt получили развилку по стадии, перечень осей пересчитан) и находки, старшие этой сессии: review-triage получил режим без метки, три списка проектных копий сведены к дому с проверяемыми копиями, пять пересказов правил стали помеченными копиями или ссылками, language.md перестал объявлять юрисдикцию над чужим плагином.
344 lines
27 KiB
Markdown
344 lines
27 KiB
Markdown
# Формат записей и индекса
|
||
|
||
Заголовок, мета-блок и строку индекса ставит `tasks.py add` — руками их не
|
||
пишут. Этот файл описывает **общую форму** любой записи и то, что проверяет
|
||
`check`; тело дописывает агент.
|
||
|
||
Чем разделы тела отличаются от типа к типу, какой алгоритм у каждого типа и что
|
||
у него обязательно — **отдельным файлом на тип**:
|
||
|
||
| Тип | Файл | Одной строкой |
|
||
| --- | --- | --- |
|
||
| ✨ `feature` | [task-feature.md](task-feature.md) | снаружи появляется то, чего не было |
|
||
| 🐞 `fix` | [task-fix.md](task-fix.md) | поведение расходится с заявленным |
|
||
| 🧹 `chore` | [task-chore.md](task-chore.md) | обслуживание, поведение не меняется |
|
||
| 🔬 `research` | [task-research.md](task-research.md) | исход — знание, а не изменение |
|
||
|
||
## Файл записи
|
||
|
||
`items/<slug>.md`:
|
||
|
||
```markdown
|
||
# 🐞 Не отбрасывать молча лишние символы в ходе
|
||
|
||
- **Тип:** fix
|
||
- **Категория:** Ядро — вернулась из работы: остаток писал нерешённое в журнал
|
||
- **Зачем:** ввод «а1б2» ходит в a1 — игрок не видит, что ошибся, и винит игру
|
||
|
||
Разбор хода читает первые два символа и молча выбрасывает остаток строки.
|
||
|
||
## Воспроизведение
|
||
|
||
Ввести `а1б2` в свой ход: программа ходит в `a1` и ничего не сообщает.
|
||
Ожидалось — отказ с ошибкой разбора.
|
||
|
||
## Затрагивает
|
||
|
||
Разбор строки хода; текст ошибки в выводе партии. Формат сохранения партии
|
||
не трогается.
|
||
|
||
## Критерии приёмки
|
||
|
||
- ввод «а1б2» отвергается с ошибкой — оракул: тест разбора
|
||
- ввод «а1» принимается по-прежнему — оракул: тест разбора
|
||
|
||
## Рамки
|
||
|
||
Схема не трогается; данные только читаются; перезапуск допустим.
|
||
|
||
Связано: решение о канонической форме содержимого.
|
||
```
|
||
|
||
- **Заголовок H1** — он же заголовок строки в индексе, дословно. Начинается
|
||
**эмодзи типа**, и она **производна**: её ставит `add` и чинит `check --fix`
|
||
по полю меты. Второго дома у типа нет — эмодзи это его отображение, как
|
||
строка индекса это отображение файла.
|
||
- **Форма заголовка — по типу.** `feature`, `fix` и `chore` отвечают на «что
|
||
нужно сделать», глаголом в неопределённой
|
||
форме, перед ним допускается «не»; `research` называет предмет разведки и
|
||
формы действия **не несёт намеренно**. Почему так — SKILL.md, «Как написана
|
||
задача». `check` считает заголовки не в форме действия и печатает число в
|
||
здоровье; годность формулировки смотрит агент `task-form`.
|
||
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательны
|
||
**тип** и **место**, причина после тире желательна (именно она объясняет,
|
||
почему задача здесь оказалась — в том числе «вернулась из работы: …»), «зачем» и
|
||
теги необязательны. Нераспознанные поля сохраняются: скрипт правит свои и не
|
||
трогает чужие.
|
||
- **Тип — первым полем.** Он решает, что у записи вообще может быть: какие
|
||
разделы обязательны и берётся ли она в работу, — и читается раньше всего
|
||
остального. Словарь **закрыт**: `feature` | `fix` | `chore` | `research`. Не подходит ни один — это сигнал, что в записи их два и
|
||
её надо разделить.
|
||
- **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль.
|
||
Не пересказ задачи: пересказ уже есть по ссылке. Живёт здесь, а не только в
|
||
индексе: строка индекса его повторяет и производна от него, `check` сверяет,
|
||
`check --fix` восстанавливает пропавшую строку **вместе с ним**. Пока поле
|
||
лежало только в индексе, штатная починка дрейфа теряла его молча и
|
||
навсегда — а это единственное, по чему задачу выбирают, не открывая.
|
||
- **Тело** — одна фраза «что станет наблюдаемо иначе», дальше разделы по схеме
|
||
типа. Пишется на языке документации проекта: предметно, без англицизмов, у
|
||
которых есть русское слово, и без терминов, которых нет ни в паспорте, ни в
|
||
архитектуре, ни в конвенциях (правило и его причина — в SKILL.md, раздел «Как
|
||
написана задача»).
|
||
|
||
Тело — не план реализации и не спецификация: принятое и реализованное переезжает
|
||
в документацию проекта, а файл задачи удаляется.
|
||
|
||
### Поле места: «Категория»
|
||
|
||
Поле называет **секцию беклога, в которой числится строка** — полку домена
|
||
(`Ядро`, `Инфра`, …), куда задачу положили и куда вернут, если она уйдёт в работу
|
||
и вернётся. **На стройке секция одна**, и поле называет её же: различать ей
|
||
нечего, но производность от заголовка индекса сохраняется и там.
|
||
|
||
Прежнее имя поля — **«Секция»**: так оно называлось у целей, указывая на часть
|
||
роадмапа. Разбор его по-прежнему принимает, `check` называет дрейфом, `check
|
||
--fix` переименовывает.
|
||
|
||
Имя самого места принадлежит **заголовку индекса** — файл на него лишь
|
||
ссылается, и принадлежность сверяется по нижнему регистру.
|
||
|
||
### Прежние формы, которые читаются, но не пишутся
|
||
|
||
Всё это `check` называет дрейфом, а `check --fix` переписывает:
|
||
|
||
| Было | Стало |
|
||
| --- | --- |
|
||
| префикс `[goal]` / `[idea]` в H1 | поле **Тип** + эмодзи в H1; `[idea]` → `research` |
|
||
| тег `kind:<род>` | поле **Тип** (род работы стал типом) |
|
||
| поле **Секция** | поле **Категория** |
|
||
| теги `goal:<слаг>` и `decomposed` | сняты: целей больше нет |
|
||
| поле **Хук** | поле **Зачем** |
|
||
| мета одной строкой через `·` | мета списком, поле на строку |
|
||
|
||
Чего `--fix` не делает сам — **решает за человека, каким быть типу**. Случаев
|
||
три, и все три уезжают пометкой `НЕОДНОЗНАЧНО`: тип, которого неоткуда взять
|
||
(`feature` от `chore` машина не отличает); тип вне словаря; и запись типа `goal`
|
||
— целей больше нет, а во что превращается эта, в задачу или в ничто, машина не
|
||
знает. Подставленное наугад значение врало бы ровно там, где по нему принимают
|
||
решение. Мёртвые теги и имя поля места при этом снимаются у **любой** записи,
|
||
включая ту, чей тип остался неразобранным.
|
||
|
||
### Затрагивает
|
||
|
||
Перечень **границ**, которых изменение касается. Границей считается то, у чего
|
||
есть внешняя сторона и цена изменения:
|
||
|
||
- эндпоинт, команда, форма ответа, код ответа;
|
||
- таблица, поле, миграция, формат на диске, формат сообщения в очереди;
|
||
- публичный тип или функция пакета, конфиг и его образцы;
|
||
- внешний сервис или библиотека, чьё поведение становится нужным.
|
||
|
||
Ничего из этого не трогается — так и пишется: «границ не трогает, изменение
|
||
внутри одного узла». Это ответ, а не пустой раздел.
|
||
|
||
**Границы, а не замысел.** «Переписать хранилище на новый драйвер» — замысел;
|
||
`таблица points и её миграция`, `эндпоинт POST /ingest` — границы. Разница
|
||
проверяется вопросом «это можно назвать до того, как решено *как* делать?»: если
|
||
нет, строка описывает реализацию, и её место в предложении об изменении.
|
||
|
||
**Свойства репозитория сюда не пишутся** — по той же причине, что и в рамки:
|
||
имя таблицы стабильно, номер последней миграции протухает молча. Пишется
|
||
`таблица points и её миграция`, а не `миграция 0042`.
|
||
|
||
**Что из этого механизировано.** `ready` смотрит только на
|
||
**наличие непустого раздела**. Полнота перечня машине не видна: границу, которую
|
||
забыли назвать, она от отсутствующей не отличает. Раздела нет — отказ во взятии:
|
||
оценивать нечем.
|
||
|
||
**У `research` раздела нет** — её границы становятся известны, когда из разведки
|
||
родятся задачи.
|
||
|
||
### Критерии приёмки
|
||
|
||
2–5 проверяемых утверждений **списком** `- …`, **у каждого назван оракул**. Не
|
||
«работает корректно», а «повторный прогон даёт тот же отпечаток — оракул:
|
||
команда сверки». Это не второе определение сделанного, а проектная
|
||
конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже:
|
||
там сказано «признак завершённости», здесь — «признак плюс чем проверяется».
|
||
|
||
**Что из этого механизировано.** `ready` считает пункты: меньше
|
||
двух — отказ («— работает» одной строкой больше не проходит), больше пяти —
|
||
замечание, обычно это признак, что задача крупнее задачи. Наличие оракула
|
||
проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только
|
||
замечание: настоящий оракул от слова «оракул» машина не отличает, и делать вид,
|
||
что проверено больше проверенного, хуже, чем не проверять вовсе.
|
||
|
||
**У `research` критериев нет** — её приёмка это записанный ответ, и описывается
|
||
она разделами «Вопрос» и «Куда ляжет ответ».
|
||
|
||
**Критерии — пол, но расхождение с ними есть дефект критериев.** Если приёмщик
|
||
видит, что критерии закрыты, а суть задачи не достигнута, он **правит критерии и
|
||
возвращает задачу исполнителю**, а не держит невидимое сверх-требование. Иначе
|
||
исполнитель никогда не знает, закончил ли, и мотивирован занижать критерии
|
||
заранее.
|
||
|
||
### Рамки
|
||
|
||
Одна строка: чего касаться нельзя, что перезапускается, что считается
|
||
необратимым, трогается ли схема данных. Раздел **допустим у любого типа задачи и
|
||
ни у одного не обязателен**. **Свойства репозитория сюда не пишутся** — номер
|
||
последней миграции, версия зависимости, хеш: в лежалой задаче они протухают
|
||
молча и становятся ложной рамкой. Снимок берётся при постановке, а не при
|
||
заведении.
|
||
|
||
### Вопросы
|
||
|
||
Неразобранное решение человека живёт разделом `## Вопросы` **плюс тегом
|
||
`question`**. Раздел без тега или тег без раздела — дрейф, `check` о нём скажет.
|
||
|
||
Раздел `Вопросы` (о решении человека) и раздел `Вопрос` у `research` (предмет
|
||
разведки) — **разные вещи и разные слова**: первый блокирует взятие, второй его
|
||
разрешает.
|
||
|
||
**Судит факт, а не метка.** Отказ во взятии даёт **непустой раздел «Вопросы»**,
|
||
независимо от того, стоит ли тег: иначе забывший тег проходил бы, а поставивший
|
||
спотыкался — стимул ровно обратный записанному правилу. Тег производен: он нужен
|
||
отбору снаружи файла (`list --questions`, `list --tag question`), и его
|
||
отсутствие при непустом разделе — замечание, а не лазейка. Тег без раздела тоже
|
||
отказ, но с другим советом: либо вопрос записан не туда, либо тег пора снять.
|
||
|
||
**Ответ на вопрос — три правки, и первая обязательна.** Раздел «Вопросы»
|
||
опустошается: ответ переезжает в тело решением, а не остаётся вопросом рядом с
|
||
ответом. Затем снимается тег (`edit <slug> --rm-tag question`) и переписывается
|
||
«зачем»: «Решено: …» на вопрос «зачем нужна эта задача» уже не отвечает.
|
||
|
||
**Порядок именно такой, потому что судит раздел, а не тег.** `ready`
|
||
смотрит в непустой раздел и откажет даже при снятом теге, а `check`
|
||
на снятый тег при непустом разделе посоветует тег вернуть. Снять тег, не
|
||
опустошив раздел, — значит закольцевать себя между двумя советами.
|
||
|
||
## Слаг
|
||
|
||
Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов
|
||
(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути, а не по текущей
|
||
формулировке**: заголовок будет переписан при переоценке, а слаг стоит в ссылках
|
||
из других задач, коммитов и черновиков. **Транслита не заводим** —
|
||
`tie-break-equal-completeness`, а не `taj-brejk-pri-ravnoj-polnote`: транслит
|
||
нечитаем для того, кто ищет по смыслу, и не сокращается.
|
||
|
||
Переименование слага — не правка, а перенос ссылок: делается одним атомарным
|
||
проходом по всем местам, где слаг упомянут, иначе останутся битые ссылки,
|
||
которых никто не проверяет.
|
||
|
||
## Индекс
|
||
|
||
Строка одной формы:
|
||
|
||
```markdown
|
||
- [🐞 Заголовок дословно](items/slug.md) — зачем
|
||
```
|
||
|
||
«Зачем» отвечает на «зачем нужна эта задача» одним предложением: состояние,
|
||
остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке.
|
||
Эмодзи внутри квадратных скобок не украшение: заголовок копируется **дословно**,
|
||
и тип виден там, где решают «брать или не брать».
|
||
|
||
| Файл | Что отвечает | Секции |
|
||
| --- | --- | --- |
|
||
| `BACKLOG.md` | что **можно взять**, в значимом порядке | называет проект; на стройке ровно одна (умолчание `План`), на доработке сколько нужно (умолчание `Ядро`/`Инфра`) |
|
||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||
|
||
Секции — **единственные заголовки `##` в индексе**: любой другой `##` в
|
||
преамбуле проверка сочтёт секцией.
|
||
|
||
**Порядок строк внутри секции значим, и стадия решает, что он значит:** на
|
||
стройке зависимость, на доработке важность (SKILL.md, «Две стадии»). Назначает
|
||
его человек — раскладывая шаги или на груминге, — и двигают его `move --after`
|
||
и `move --first`. Одно место из очереди изъято и **производно от типа и
|
||
заполненности**: **сырьё** (`research` без раздела «Вопрос») стоит в конце своей
|
||
секции, потому что его не берут, и между берущимся оно каждый раз требует
|
||
открыть файл, чтобы это понять. Проверяет `check`, переставляет `check --fix`,
|
||
и человек этот порядок не назначает — иначе он был бы решением, которого здесь
|
||
нет.
|
||
|
||
**Секции «блокеры» среди них нет.** Блокер — состояние, а не полка: он живёт до
|
||
ответа человека, а следы остаются вопросами в файлах задач.
|
||
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
|
||
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
|
||
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её.
|
||
|
||
**Имена секций проект выбирает сам, а количество ограничено стадией:** на
|
||
стройке секция одна, потому что порядок там зависимость, и разложенный по полкам
|
||
список перестаёт быть планом. Проверяет `check`; слить секции сам он не берётся —
|
||
в каком порядке пойдут строки слитых полок, знает только человек.
|
||
|
||
**Заголовок секции пишется с прописной и отбивается пустой строкой с обеих
|
||
сторон.** Отбивку правит `check --fix`; он же сводит написание места в мете файла
|
||
с заголовком индекса.
|
||
|
||
Индекс **производен**: расходится с файлом — правим индекс (`check --fix`).
|
||
Строку руками не пишут.
|
||
|
||
Отсюда же ответ на «а если оборвётся посередине». Мутация сперва проверяет всё
|
||
и складывает правки, и только потом пишет: сначала все временные файлы, потом
|
||
переименования подряд. Полной транзакции на несколько файлов файловая система не
|
||
даёт, но окно сжато до цепочки переименований, а **всё, что в нём может
|
||
разъехаться, — производное**: файлы целы, индекс восстанавливает `check --fix`.
|
||
Поэтому отказ на второй задаче из пяти не оставляет первую переписанной при
|
||
нетронутом индексе.
|
||
|
||
## `REJECTED.md`
|
||
|
||
Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет
|
||
`tasks.py close --reason`, а `check` следит за форматом:
|
||
|
||
```markdown
|
||
- 2026-07-23 `versii-kachestvo-repaki` — Версии и качество одного тайтла.
|
||
Причина: калибровка болей — не боль, ни разу не возникло за полгода.
|
||
Была секция: Инфра.
|
||
```
|
||
|
||
Реализованные сюда не попадают: у них остаётся коммит и документация. У
|
||
выкинутой не остаётся ничего — и через квартал она возвращается тем же текстом.
|
||
Это первое место, куда смотрит дедупликация при заведении.
|
||
|
||
Запись не запрещает завести задачу заново: изменился контекст — заводим и
|
||
ссылаемся на строку, объясняя, что изменилось.
|
||
|
||
## Теги
|
||
|
||
Разметка сверх типа. Тип полем, потому что он один и обязателен; теги — потому
|
||
что их много и `list --tag` уже умеет отбирать по ним порцию разбора.
|
||
|
||
- `question` — в файле есть неразобранный раздел «Вопросы».
|
||
|
||
Тегов `kind:<род>`, `goal:<слаг>` и `decomposed` больше нет: род работы стал
|
||
типом, а цели упразднены. Оставшиеся в файле `check` называет дрейфом, а `check
|
||
--fix` снимает (значение `kind:` при этом переезжает в поле «Тип»).
|
||
|
||
Отбор — `list --tag a,b`: перечисленные через запятую теги требуются **все
|
||
сразу** (это И, не ИЛИ). Тег, которого нет ни у одной задачи, `list` называет
|
||
вслух: молчаливый ноль читается как «таких задач нет», а чаще это опечатка.
|
||
|
||
Свои теги проект заводит свободно (партия ревью `review-ГГГГ-ММ-ДД`, тема,
|
||
источник) — словарь не фиксирован. В индекс теги не выносим: он
|
||
производен, отбор делает `list --tag`, а не глаза.
|
||
|
||
## Тест «готова к взятию»
|
||
|
||
Задача готова, если из файла отвечаются три вопроса. Первый общий, второй и
|
||
третий у каждого типа свои и перечислены в его файле.
|
||
|
||
1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю,
|
||
владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет
|
||
ломаться Y при Z» — ответ. **У `chore` адресат — разработчик, и это
|
||
законно**: «уедет последний вызов устаревшего API» — ответ, а не отговорка.
|
||
Тип объявлен как раз затем, чтобы такие задачи не выдумывали себе
|
||
пользовательскую пользу.
|
||
2. **Что известно про сегодня** — то, что тип требует знать до работы:
|
||
у `fix` это `Воспроизведение`, у `research` — `Вопрос`, у `feature` и
|
||
`chore` — `Затрагивает`.
|
||
3. **По чему видно, что закончено** — критерии приёмки с оракулами;
|
||
у `research` вместо них `Куда ляжет ответ`.
|
||
Не отвечается любой из трёх → это ещё не задача, а **сырьё**: тип `research` без
|
||
раздела «Вопрос», место — конец секции, работа над ним — штурм.
|
||
|
||
Отвечается всё, но задача не делается одним заходом и не мерджится целиком →
|
||
это **несколько задач**, дроби сразу и ставь их в списке подряд. Промежуточного
|
||
зонтика между планом и задачей нет: тип `[epic]` упразднён, и цель, ставшая
|
||
зонтиком после него, упразднена тоже.
|
||
|
||
Тест применяется при заведении и при переоценке. К старым задачам, которых
|
||
операция не касается, задним числом не применяется — беклог не переоформляют
|
||
«заодно».
|