форма записи: заголовок отвечает на вопрос своего типа
Обкатка скилла tasks на выдуманном проекте — консольные крестики-нолики на JavaScript, каталог заведён с нуля тем же скриптом. Форма вылезла раньше содержания, и правки все про неё. Заголовок отвечает на вопрос типа записи, и форм три: цель — утверждение о возможности, задача — глагол в неопределённой форме (допускается «не» перед ним), идея — назывное, без обещания. Причина не стилистическая: описательный заголовок называет состояние, а из состояния не видно, чего от работы ждут — «Ничья объявляется, пока клетки есть» одинаково читается как жалоба и как задание. Отсюда же разница индексов: роадмап — список возможностей, беклог — список работ, и перепутанные формы делают каждый похожим на другой. Механизировано ровно то, что механизируется: check считает заголовки, где первое слово не на -ть/-ти/-чь, и печатает число в блоке здоровья. Замечанием на файл нельзя — эвристика грубая, а на 97 записях двух живых проектов это поток одинаковых строк, после которого пропускают весь блок. Годность формулировки судит отдельный агент task-wording, а не чек-лист в скилле: сейчас формулировку пишет и проверяет один агент в одном контексте, а самопроверка текста слабее всего там, где формулировка казалась удачной при написании. Он ничего не правит — возвращает готовые формулировки, и заголовок с «зачем» показываются человеку, потому что по ним задачу выбирают. Ничего из того, что ловит tasks.py check, он не трогает намеренно: это был бы второй дом для правила. Заголовки секций — с прописной, после заголовка пустая строка, во всех индексах. Канонические имена стали Готово | Запланировано | Направления | Разработка (англ. Done | Planned | Directions | Tooling), сверка везде по нижнему регистру, так что старые индексы читаются по-прежнему. Отбивка живёт на записи, а не на вставке: через Plan.index проходит каждая правка индекса, а мест вставки три. Имя секции принадлежит заголовку индекса, файл на неё только ссылается. Это разрешает единственную неоднозначность починки — расхождение в одном регистре правится в пользу заголовка. Без него переезд на канон оставил бы «Готово» в роадмапе и «готово» в каждом файле цели, и свести это было бы некому. Регистр правится только у канонических секций: имена секций беклога выбирает проект. Обкатка нашла два дефекта, которых не находили ни линтеры, ни свои проверки. Вставка в пустую секцию съедала отбивку перед следующим заголовком — пропуск пустых строк теперь идёт только до первой непустой. Мета, разорванная пустой строкой, теряла поля молча: check видел лишь следствие («без рода работы») и советовал edit --kind, который дописывал второе такое же поле. Поле меты в теле стало ошибкой с названной причиной, и --fix её намеренно не чинит — какое из двух значений верное, знает человек. DECISIONS тема 20 (ЕЕЕ–ККК, следствия 82–85), changelog канона v3 пополнен двумя пунктами и двумя шагами переезда, TODO — два шага для healthlog и jellybit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+93
-10
@@ -1407,27 +1407,27 @@ SSS: рубрика на узел без нового понятия порож
|
||||
словаре, тесте готовности, автомате переходов, `split.md` и трёх местах
|
||||
`tasks.py`.
|
||||
|
||||
**ГГГ. Имена секций роадмапа — `готово` / `запланировано` / `направления` /
|
||||
`разработка`.** Первый набор (`умеет` / `строим` / `станок`) прожил один заход и
|
||||
**ГГГ. Имена секций роадмапа — `Готово` / `Запланировано` / `Направления` /
|
||||
`Разработка`.** Первый набор (`умеет` / `строим` / `станок`) прожил один заход и
|
||||
был признан неудачным. Из четырёх предложенных имён отвергнуто одно, и по
|
||||
проверяемой причине: **`окружение` уже занято** — в `architecture.md` это боевое
|
||||
окружение приложения, «где работает, что рядом, кто перезапускает», и одно слово
|
||||
в двух смыслах развело бы документы канона. Взято `разработка`.
|
||||
в двух смыслах развело бы документы канона. Взято `Разработка`.
|
||||
|
||||
Принятый компромисс назван вслух: `готово` слегка тянет обратно в трекерную рамку
|
||||
Принятый компромисс назван вслух: `Готово` слегка тянет обратно в трекерную рамку
|
||||
«состояние работы», тогда как секция про **возможность**. Перевесила читаемость с
|
||||
первого взгляда, а смысл несут заголовки целей внутри секции. Так же принято, что
|
||||
цель в `запланировано` может быть уже наполовину построена: это очередь, а не
|
||||
цель в `Запланировано` может быть уже наполовину построена: это очередь, а не
|
||||
«не начато», а «в работе» живёт в `SPRINT.md`.
|
||||
|
||||
**ДДД. Секции роадмапа канонические, секции беклога — нет.** Разница выведена, а
|
||||
не назначена: у секций роадмапа есть **семантика** (достигнутое, очередь, долгое,
|
||||
не про продукт), в первую пишет сам `close`, и роадмап, названный по-своему,
|
||||
читался бы только своим автором. Секции беклога (`ядро`, `инфра`) семантики не
|
||||
читался бы только своим автором. Секции беклога (`Ядро`, `Инфра`) семантики не
|
||||
несут — это полки. Поэтому `check` проверяет у роадмапа три вещи: состав закреплён
|
||||
(чужая секция — ошибка), все четыре обязаны быть, язык один на весь индекс;
|
||||
`--roadmap-sections` у `init` упразднён. Английский набор — `done` | `planned` |
|
||||
`directions` | `tooling`.
|
||||
`--roadmap-sections` у `init` упразднён. Английский набор — `Done` | `Planned` |
|
||||
`Directions` | `Tooling`.
|
||||
|
||||
Проверено на том самом случае, ради которого правило и заводилось: секция «Что
|
||||
уже пройдено», которую healthlog вёл руками, теперь называется ошибкой поимённо.
|
||||
@@ -1442,8 +1442,91 @@ SSS: рубрика на узел без нового понятия порож
|
||||
80. **Прозаический раздел в индексе — дрейф.** Любой `##` проверка считает
|
||||
секцией, поэтому «Что уже пройдено» и «Почему в таком порядке» в healthlog
|
||||
формально были двумя лишними секциями, куда могла уехать задача. При
|
||||
повышении они разбираются: звенья — строками в `умеет`, обоснование очереди —
|
||||
прозой внутри `строим`.
|
||||
повышении они разбираются: звенья — строками в `Готово`, обоснование очереди —
|
||||
прозой внутри `Запланировано`.
|
||||
81. **Правил стало пять, и нулевое — про смысл, а не про механику.** «Цель —
|
||||
возможность, задача — шаг к ней» стоит перед правилами о гниении беклога и
|
||||
производности индексов, потому что из него следует, зачем эти механики нужны.
|
||||
|
||||
## 20. Форма записи: заголовок, секции, вычитка (2026-08-04)
|
||||
|
||||
### Что было
|
||||
|
||||
Обкатка обновлённого скилла на выдуманном проекте — консольные крестики-нолики
|
||||
на JavaScript. Каталог задач заведён с нуля тем же скриптом: шесть целей, девять
|
||||
задач, отказ, достижение цели, спринт. Смотрели три вещи: тексты, разделы, состав
|
||||
задач.
|
||||
|
||||
Форма вылезла раньше содержания. Индексы вышли с секциями со строчной буквы и без
|
||||
отбивки после заголовка — читается как список списков, а не как документ. А все
|
||||
заголовки задач оказались **описательными**: «Лишние символы в ходе молча
|
||||
отбрасываются», «Поле печатается одним куском кода», «Линтер и тесты гоняются
|
||||
одной командой». Правило «задача отвечает на «что для этого нужно сделать»» в
|
||||
скилле стояло с самого начала — но относилось к содержанию задачи, а не к её
|
||||
заголовку, и потому не применялось там, где заголовок и есть всё, что видно в
|
||||
списке.
|
||||
|
||||
### Решено
|
||||
|
||||
**ЕЕЕ. Заголовок отвечает на вопрос своего типа, и форм три.** Цель — утверждение
|
||||
о возможности («Соперником может быть компьютер»); задача — глагол в
|
||||
неопределённой форме, допускается «не» перед ним («Не отбрасывать молча лишние
|
||||
символы в ходе»); идея — назывное, без обещания. Причина не стилистическая:
|
||||
описательный заголовок называет **состояние**, а из состояния не видно, чего от
|
||||
работы ждут — «Ничья объявляется, пока клетки есть» одинаково читается как жалоба
|
||||
и как задание. В списке, где решают «брать или не брать», это разные вещи.
|
||||
|
||||
Отсюда же разница индексов: роадмап — список возможностей, беклог — список работ.
|
||||
Перепутанные формы заголовков делают каждый из них похожим на другой.
|
||||
|
||||
**ЖЖЖ. Механизировано ровно то, что механизируется, — счётчиком, а не
|
||||
замечанием.** `check` считает заголовки, у которых первое слово не оканчивается
|
||||
на `-ть`/`-ти`/`-чь` (перед ним допускается «не»), и печатает **число** в блоке
|
||||
здоровья. Замечанием на файл этого делать нельзя: проверка эвристическая, а
|
||||
беклог, заведённый до правила, переоформляют не «заодно» — десятки одинаковых
|
||||
строк научили бы пропускать весь блок.
|
||||
|
||||
**ЗЗЗ. Годность формулировки судит отдельный агент `task-wording`, а не чек-лист
|
||||
в скилле.** Самопроверка текста слабее всего там, где формулировка казалась
|
||||
удачной при написании, — а пишет и проверяет иначе один и тот же агент в одном
|
||||
контексте. Агент читает пачку записей и возвращает **готовые формулировки на
|
||||
замену**, ничего не правя сам; заголовок и «зачем» подставляются командой и
|
||||
показываются человеку, потому что именно по ним задачу выбирают. Он намеренно не
|
||||
проверяет ничего из того, что ловит `tasks.py check`: повторить машинную проверку
|
||||
словами значит завести правилу второй дом.
|
||||
|
||||
**ИИИ. Заголовок секции — с прописной, после него пустая строка.** Во всех
|
||||
индексах, включая секции беклога, имена которых выбирает проект: правило про
|
||||
**оформление**, а не про имя. Канонические имена стали писаться с прописной
|
||||
(`Готово` | `Запланировано` | `Направления` | `Разработка`, англ. `Done` |
|
||||
`Planned` | `Directions` | `Tooling`), сверка везде идёт по нижнему регистру, так
|
||||
что старые индексы читаются по-прежнему и поднимаются `check --fix`.
|
||||
|
||||
**ККК. Имя секции принадлежит заголовку индекса, файл на неё только ссылается.**
|
||||
Это разрешает единственную неоднозначность починки: расхождение файла и заголовка
|
||||
**в одном регистре** правится в пользу заголовка. Без этого шага переезд на канон
|
||||
оставил бы `Готово` в роадмапе и `готово` в каждом файле цели — расхождение
|
||||
безвредное, но вечное, потому что свести его было бы некому.
|
||||
|
||||
### Что из этого следует
|
||||
|
||||
82. **Отбивка живёт на записи, а не на вставке.** `spaced_sections` вызывается в
|
||||
`Plan.index`, через который проходит **каждая** запись индекса. Чинить
|
||||
отбивку в каждом месте вставки значило бы полагаться на то, что ни одного не
|
||||
забыли, — а мест вставки три (`--first`, `--after`, в конец).
|
||||
83. **Обкатка нашла два дефекта, которых не нашли ни линтеры, ни свои проверки.**
|
||||
Вставка в пустую секцию съедала отбивку перед следующим заголовком; мета,
|
||||
разорванная пустой строкой, теряла поля молча, а `check` видел только
|
||||
следствие («без рода работы») и советовал `edit --kind`, который дописывал
|
||||
**второе** такое же поле. Оба класса теперь названы: пропуск пустых строк
|
||||
идёт только до первой непустой, а поле меты в теле — ошибка с названной
|
||||
причиной, которую `--fix` намеренно не чинит.
|
||||
84. **Пустой проект показывает форму хуже живого.** Чтобы увидеть достигнутую
|
||||
цель, отказ, спринт и все четыре рода работы, проект пришлось поставить на
|
||||
середину пути. Это довод в пользу того, чтобы обкатку вести на *состоянии*, а
|
||||
не на *старте*: у старта половина формы не наблюдаема.
|
||||
85. **Мелкая цель даёт две задачи, и это не повод её укрупнять.** У цели
|
||||
«Соперником может быть компьютер» третья задача напрашивалась (выбор уровня
|
||||
соперника), но не мерджится порознь: без сильного соперника выбирать не из
|
||||
чего. Тест декомпозиции её отбил — частичный ответ на вопрос «не выродились
|
||||
ли цели в ярлыки тем».
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
- `docs` — содержимое канона по ходу разработки: ADR из архивного
|
||||
`design.md`, промоут конвенций, запись в разведку и журнал ревью, чистка
|
||||
архитектуры;
|
||||
- `tasks` — задачи и цели каталогом markdown-файлов;
|
||||
- `tasks` — задачи и цели каталогом markdown-файлов; вычитку формулировок
|
||||
ведёт отдельный агент `task-wording`;
|
||||
- `session` — ритуал между спринтами и ведение спринта.
|
||||
- **av-dev-pipeline** — исполнение. **Требует OpenSpec.**
|
||||
- `task-pipeline` — задача через полный цикл SDD, от постановки до коммита;
|
||||
|
||||
@@ -167,11 +167,17 @@ jellybit 43. Шаги повышения — [changelog.md](av-dev-pm/skills/can
|
||||
- [ ] род работы и раздел «Затрагивает» — **не задним числом**: сперва то, что
|
||||
идёт в ближайший набор (`sprint take` без них откажет), остальное по ходу
|
||||
переоценки (PPP)
|
||||
- [ ] секции роадмапа: `порядок` → `запланировано`, `темы` → `направления`,
|
||||
завести `готово` и `разработка`; прозаические разделы healthlog («Что уже
|
||||
- [ ] секции роадмапа: `порядок` → `Запланировано`, `темы` → `Направления`,
|
||||
завести `Готово` и `Разработка`; прозаические разделы healthlog («Что уже
|
||||
пройдено», «Почему в таком порядке») разложить — звенья строками в
|
||||
`готово`, обоснование очереди прозой внутри `запланировано` (тема 19, 80).
|
||||
`Готово`, обоснование очереди прозой внутри `Запланировано` (тема 19, 80).
|
||||
`check` теперь называет чужую секцию ошибкой, так что шаг обязателен
|
||||
- [ ] переформулировать цели ответом на «что приложение будет уметь»; цели не
|
||||
про приложение («Процесс и качество разработки» в jellybit) — в
|
||||
`разработка`
|
||||
`Разработка`
|
||||
- [ ] `check --fix` на обоих: поднимет написание канонических секций, поставит
|
||||
отбивку после заголовков и сведёт секцию в мете файлов с заголовками.
|
||||
Секции беклога переименовать руками — имена выбирал проект (тема 20, ККК)
|
||||
- [ ] заголовки задач в форму действия — **не задним числом**: по мере попадания
|
||||
задачи в работу. `check` печатает их число, `task-wording` предложит
|
||||
формулировки пачкой (тема 20, ЕЕЕ)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: task-wording
|
||||
description: "Вычитка формулировок задач, целей и идей: форма заголовка по типу записи (цель — что приложение будет уметь, задача — что нужно сделать, идея — о чём она), англицизм при живом русском слове, термин, которого нет в документах проекта, «зачем», пересказывающее заголовок вместо состояния и боли, «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах. Отдаёт готовые формулировки на замену и ничего не правит сам. Использовать после заведения или разбора пачки записей и на переоценке. Только чтение."
|
||||
tools: Read, Grep, Glob
|
||||
model: sonnet
|
||||
color: green
|
||||
---
|
||||
|
||||
Ты — **вычитка формулировок** каталога задач. Оптика — язык записи, а не работа,
|
||||
которую она описывает: ты не судишь, нужна ли задача, правильно ли выбрана цель и
|
||||
достаточно ли её декомпозиции.
|
||||
|
||||
Ты **ничего не правишь**. Каждая находка — готовая формулировка на замену,
|
||||
которую зовущий подставит командой (`edit <слаг> --title …`, `--why …`) или
|
||||
впишет в тело. Файлы ты только читаешь.
|
||||
|
||||
## Что тебе дают
|
||||
|
||||
Список файлов записей (`items/<slug>.md`) или каталог задач целиком. Плюс, если
|
||||
зовущий их назвал, документы проекта — паспорт, архитектура, конвенции: по ним
|
||||
проверяется, известен ли термин. **Не назвали — считай известными только те
|
||||
слова, что встречаются в других записях того же каталога**, и говори об этом в
|
||||
границах покрытия.
|
||||
|
||||
## Правила
|
||||
|
||||
Проверяешь семь, и у каждого своя причина — она объясняет, где правило **не**
|
||||
применяется.
|
||||
|
||||
1. **Форма заголовка по типу записи.**
|
||||
|
||||
| Тип | Отвечает на | Форма |
|
||||
| --- | --- | --- |
|
||||
| `[goal]` | что приложение будет уметь | утверждение о возможности: «Соперником может быть компьютер» |
|
||||
| задача | что нужно сделать | глагол в неопределённой форме, допускается «не» перед ним: «Печатать поле одним куском кода» |
|
||||
| `[idea]` | о чём она | назывное, без обещания: «Подсказка следующего хода» |
|
||||
|
||||
Описательный заголовок задачи («Лишние символы молча отбрасываются») называет
|
||||
**состояние** и одинаково читается как жалоба и как задание. Заголовок цели в
|
||||
форме действия («Сделать соперника-компьютер») превращает роадмап в список
|
||||
работ — а он список возможностей.
|
||||
|
||||
**Область работ — не цель.** «Работа со слиянием», «Рефакторинг вывода» не
|
||||
отвечают ни на один из трёх вопросов; предложи возможность, которую эта
|
||||
работа создаёт, и скажи, если из текста её не видно.
|
||||
|
||||
2. **«Зачем» отвечает на «зачем нужна эта задача»** — состояние, остаток, боль,
|
||||
— а не пересказывает заголовок. «Починить разбор хода» при заголовке «Не
|
||||
отбрасывать молча лишние символы» — пересказ: читающий узнаёт то же самое
|
||||
дважды и по-прежнему не знает, почему это лежит в беклоге.
|
||||
|
||||
3. **Англицизм, у которого есть живое русское слово, заменяется.** Не
|
||||
«зафиксить флоу», а «починить порядок доставки»; не «отрефакторить», а
|
||||
«убрать второй путь приёма». **Не трогай** то, что является именем вещи: слаг,
|
||||
имя пакета, команда, тип в коде, устоявшийся термин предметной области.
|
||||
|
||||
4. **Термин, которого нет в документах проекта, вводится одной строкой или не
|
||||
употребляется.** Заменять его своей догадкой нельзя: ты не знаешь предметную
|
||||
область. Пиши «термин «X» не встречается ни в документах, ни в других
|
||||
записях — введи строкой или назови известным словом».
|
||||
|
||||
5. **«Затрагивает» перечисляет границы, а не замысел.** Граница — то, у чего есть
|
||||
внешняя сторона: команда и её аргументы, эндпоинт, таблица и миграция, формат
|
||||
на диске, публичный тип пакета, внешний сервис. «Переписать хранилище на новый
|
||||
драйвер» — замысел; проверяется вопросом «это можно назвать до того, как
|
||||
решено *как* делать?». Свойства репозитория (номер миграции, версия
|
||||
зависимости, хеш) — тоже находка: они протухают молча.
|
||||
|
||||
6. **У критерия назван оракул, и оракул проверяем.** «Оракул: глазами» на
|
||||
утверждение, которого глазами не проверить («компьютер не проигрывает ни в
|
||||
одной партии»), — находка: слово стоит, проверки нет. Число критериев считает
|
||||
`check`, тебе оно неинтересно.
|
||||
|
||||
7. **Предписания процесса в теле нет.** «Делать профилем standard», «взять такой-то
|
||||
агент» — это выбор, который делают, увидев изменение, а не при постановке.
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
Всё, что ловит `tasks.py check`: состав и написание секций, наличие разделов,
|
||||
число критериев, теги, согласованность индексов, битые ссылки. Повторять
|
||||
машинную проверку словами — заводить второй дом для одного правила; если видишь
|
||||
такое, просто не пиши.
|
||||
|
||||
Не проверяешь и **содержание работы**: нужна ли задача, верно ли выбрана цель,
|
||||
не крупна ли она. Это разбор, а не вычитка.
|
||||
|
||||
## Порог вмешательства
|
||||
|
||||
**Правка без нарушенного правила не пишется.** Список, в котором половина —
|
||||
вкусовые переформулировки, перестают читать целиком, и вместе с ним пропадают
|
||||
настоящие находки. Сомневаешься — не пиши. Формулировка, которая просто **не
|
||||
твоя**, — не находка.
|
||||
|
||||
Одна запись может дать несколько находок, но заголовок правится один раз: не
|
||||
предлагай два варианта на выбор, предлагай лучший.
|
||||
|
||||
## Доклад
|
||||
|
||||
Находки по одной, в порядке важности (заголовок → «зачем» → границы → критерии →
|
||||
язык):
|
||||
|
||||
```
|
||||
<файл>
|
||||
правило: <номер и короткое имя>
|
||||
сейчас: <как написано>
|
||||
предложение: <готовая формулировка, подставляемая как есть>
|
||||
почему: <одна фраза>
|
||||
```
|
||||
|
||||
В конце — **границы покрытия**: сколько записей просмотрено из скольких, какие
|
||||
не смотрел и почему, и по чему проверялись термины (документы проекта названы
|
||||
или нет). Отчёт без этой строки читается как «беклог вычитан», не сообщая, какая
|
||||
его часть осталась нетронутой.
|
||||
|
||||
Ничего не нашёл — так и скажи одной строкой. Пустой доклад с границами покрытия
|
||||
полезнее выдуманной находки.
|
||||
@@ -32,20 +32,29 @@ upgrade` идёт по записям снизу вверх от версии п
|
||||
касается (эндпоинт, таблица и миграция, формат на диске, публичный тип). Как
|
||||
и критерии приёмки, требуется к взятию в спринт, а не к заведению.
|
||||
3. **Секции роадмапа** — четыре вместо двух и **канонические**, в отличие от
|
||||
секций беклога: `готово` (достигнутые цели строкой с датой, без ссылки на
|
||||
файл), `запланировано` (очередь значима), `направления` (очереди нет),
|
||||
`разработка` (инструмент и процесс, не возможности приложения). Английский
|
||||
вариант — `done` | `planned` | `directions` | `tooling`, один язык на весь
|
||||
секций беклога: `Готово` (достигнутые цели строкой с датой, без ссылки на
|
||||
файл), `Запланировано` (очередь значима), `Направления` (очереди нет),
|
||||
`Разработка` (инструмент и процесс, не возможности приложения). Английский
|
||||
вариант — `Done` | `Planned` | `Directions` | `Tooling`, один язык на весь
|
||||
индекс. Переименованию проектом не подлежат: у каждой свой смысл, и в первую
|
||||
пишет сам `close`; `tasks.py check` проверяет состав.
|
||||
4. **Умолчание профиля ревью сменилось** — это не раскладка, но проектный текст
|
||||
4. **Форма заголовка записи** — по типу: задача отвечает на «что нужно сделать»
|
||||
и пишется глаголом в неопределённой форме («Не отбрасывать молча лишние
|
||||
символы»), цель — на «что приложение будет уметь», идея просто называет, о
|
||||
чём она. `check` считает заголовки не в форме действия и печатает число в
|
||||
блоке здоровья. Годность формулировки — не машине: её смотрит новый агент
|
||||
`task-wording` (вычитка формулировок, только чтение).
|
||||
5. **Заголовки секций — с прописной, после заголовка пустая строка**, во всех
|
||||
индексах. Написание канонических секций и отбивку правит `check --fix`; он
|
||||
же сводит написание секции в мете файла с заголовком индекса.
|
||||
6. **Умолчание профиля ревью сменилось** — это не раскладка, но проектный текст
|
||||
под него уже написан. `standard` стал рабочим умолчанием: миграция схемы,
|
||||
публичный контракт и инвариант ступень больше **не** поднимают, `wide`
|
||||
означает новое понятие или структурную единицу. Подраздел «Триггеры профиля»
|
||||
в `docs/review.md` остаётся на месте, но его содержимое надо перечитать.
|
||||
|
||||
**Что переехало:** `docs/tasks/PLAN.md` → `docs/tasks/ROADMAP.md`; достигнутая
|
||||
цель — из небытия в секцию `готово`: `close <цель> --implemented` удаляет файл, но
|
||||
цель — из небытия в секцию `Готово`: `close <цель> --implemented` удаляет файл, но
|
||||
**оставляет строку с датой**. Прежде роадмап отвечал только «что осталось», и
|
||||
половину его вопроса вели прозой руками. Вместе с
|
||||
файлом переименован ключ конфига `tasks.plan` → `tasks.roadmap` и токены
|
||||
@@ -76,19 +85,26 @@ upgrade` идёт по записям снизу вверх от версии п
|
||||
что для этого проекта считается **новым понятием** и **правилом
|
||||
идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю
|
||||
частоту полного набора уточнением.
|
||||
7. Переименовать секции роадмапа: `порядок` → `запланировано`, `темы` →
|
||||
`направления`; завести `готово` **первой** и `разработка` последней.
|
||||
7. Переименовать секции роадмапа: `порядок` → `Запланировано`, `темы` →
|
||||
`Направления`; завести `Готово` **первой** и `Разработка` последней.
|
||||
Прозаические разделы вроде «Что уже пройдено», которые велись руками,
|
||||
разложить: звенья — строками в `готово` (дата, слаг, что стало возможно),
|
||||
обоснование очереди оставить прозой в `запланировано`. Любой `##` в индексе
|
||||
разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно),
|
||||
обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе
|
||||
проверка считает секцией, и теперь `check` называет чужую секцию ошибкой.
|
||||
8. Переформулировать цели ответом на **«что приложение будет уметь»**: не
|
||||
«Работа со слиянием», а «Исход слияния не зависит от порядка доставки».
|
||||
Свойство поведения — законная цель. Цель, которая не про приложение
|
||||
(процесс, инструмент), переезжает в `разработка`.
|
||||
(процесс, инструмент), переезжает в `Разработка`.
|
||||
9. `[epic]`, если он в проекте заводился: это либо цель, либо набор задач под
|
||||
общей целью. `check` назовёт его неизвестным типом.
|
||||
10. `docs/.pm.json`: `"canon": 3`.
|
||||
10. Прогнать `python3 tasks.py check --dir docs/tasks --fix`: он поднимет
|
||||
написание канонических секций, поставит отбивку после заголовков и сведёт
|
||||
секцию в мете файлов с заголовками индексов. Секции беклога проект
|
||||
переименовывает сам — их имена он выбирал, и трогать их скрипт не вправе.
|
||||
11. Переписать заголовки задач в форму действия — по мере того, как задача
|
||||
попадает в работу, а не «заодно»: `check` печатает их число, а `task-wording`
|
||||
предложит формулировки на замену пачкой.
|
||||
12. `docs/.pm.json`: `"canon": 3`.
|
||||
|
||||
## Версия 2 — 2026-08-03
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ description: "Завести новый проект — сессия вопро
|
||||
5. **Чем краснеет гейт.** Какие проверки обязательны; что красит безусловно;
|
||||
чего в гейте намеренно не будет и кто тогда это гоняет.
|
||||
6. **Первые цели.** Возможности приложения, а не задачи: три-пять целей в
|
||||
`запланировано`, каждая — ответ на «что приложение будет уметь», с
|
||||
`Запланировано`, каждая — ответ на «что приложение будет уметь», с
|
||||
обоснованием очереди прозой.
|
||||
|
||||
### Как вести
|
||||
|
||||
@@ -175,9 +175,9 @@
|
||||
|
||||
## Шаг 4. Выбор цели и набор спринта
|
||||
|
||||
1. **Покажи состояние проекта**: секцию `готово` (что приложение уже умеет —
|
||||
это половина ответа на «где мы»), затем `запланировано` с обоснованием
|
||||
очереди, `направления`, и
|
||||
1. **Покажи состояние проекта**: секцию `Готово` (что приложение уже умеет —
|
||||
это половина ответа на «где мы»), затем `Запланировано` с обоснованием
|
||||
очереди, `Направления`, и
|
||||
по каждой цели-кандидату — сколько под ней задач без открытых вопросов
|
||||
(`list --goal <слаг>`). Цель без готовых задач набором не станет: её сперва
|
||||
надо декомпозировать.
|
||||
|
||||
@@ -71,22 +71,28 @@ docs/tasks/
|
||||
|
||||
| Секция | Англ. | Что в ней |
|
||||
| --- | --- | --- |
|
||||
| `готово` | `done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках |
|
||||
| `запланировано` | `planned` | очередь значима и обосновывается прозой рядом |
|
||||
| `направления` | `directions` | очереди нет, тянутся долго |
|
||||
| `разработка` | `tooling` | инструмент и процесс — не возможности приложения, и потому отдельно |
|
||||
| `Готово` | `Done` | достигнутые цели — строкой с датой, **без ссылки на файл**: файл удалён, поведение живёт в спеках |
|
||||
| `Запланировано` | `Planned` | очередь значима и обосновывается прозой рядом |
|
||||
| `Направления` | `Directions` | очереди нет, тянутся долго |
|
||||
| `Разработка` | `Tooling` | инструмент и процесс — не возможности приложения, и потому отдельно |
|
||||
|
||||
**Секции роадмапа канонические, секции беклога — нет**, и разница не в любви к
|
||||
единообразию. У каждой секции роадмапа свой смысл, в первую пишет сам `close`, и
|
||||
роадмап, названный по-своему, читался бы только своим автором. Секции беклога
|
||||
(`ядро`, `инфра`) смысла не несут — это полки, и остаются делом проекта.
|
||||
(`Ядро`, `Инфра`) смысла не несут — это полки, и остаются делом проекта.
|
||||
|
||||
Отсюда три правила, которые проверяет `tasks.py check`: **состав закреплён**
|
||||
(чужая секция — ошибка, а не вольность), **все четыре обязаны быть** (нет
|
||||
секции — нет ответа на её часть вопроса), **язык один на весь индекс**.
|
||||
`--roadmap-sections` у `init` нет: выбирать нечего.
|
||||
|
||||
Оговорка про `разработка`: слово `окружение` сюда не годится — в
|
||||
**Заголовок секции — с прописной, после него пустая строка.** Во всех индексах
|
||||
одинаково, включая секции беклога, которые проект называет сам. Написание
|
||||
канонических секций правит `check --fix` (заодно и ссылку на секцию в мете
|
||||
файлов: имя секции принадлежит заголовку индекса, файл на неё только
|
||||
ссылается); отбивку он ставит везде.
|
||||
|
||||
Оговорка про `Разработка`: слово `окружение` сюда не годится — в
|
||||
`architecture.md` оно уже значит боевое окружение приложения, и одно слово в двух
|
||||
смыслах развело бы документы канона.
|
||||
|
||||
@@ -110,7 +116,7 @@ docs/tasks/
|
||||
всех наборов без отдельного журнала.
|
||||
|
||||
**У достигнутой цели запись остаётся, и это единственное исключение.** Файл
|
||||
удаляется так же, а строка переезжает в секцию `готово` с датой. Причина в том,
|
||||
удаляется так же, а строка переезжает в секцию `Готово` с датой. Причина в том,
|
||||
что цель — не работа, а **возможность**: «что приложение умеет» это половина
|
||||
вопроса, ради которого роадмап и открывают, и стирать её вместе с файлом значит
|
||||
оставить инструмент, отвечающий только «что осталось». Вторым домом это не
|
||||
@@ -172,9 +178,9 @@ stateDiagram-v2
|
||||
отведена отдельная секция роадмапа, чтобы они были видны в том же экране и при
|
||||
этом не читались как возможности продукта.
|
||||
|
||||
Секция выбирается так: очередь значима и обоснована прозой — `запланировано`;
|
||||
тянется долго и очереди не имеет — `направления`; не про приложение —
|
||||
`разработка`; в `готово` кладёт сам `close`.
|
||||
Секция выбирается так: очередь значима и обоснована прозой — `Запланировано`;
|
||||
тянется долго и очереди не имеет — `Направления`; не про приложение —
|
||||
`Разработка`; в `Готово` кладёт сам `close`.
|
||||
|
||||
- **Список задач цели выводится, а не хранится.** В теле цели — зачем она и что
|
||||
считается её завершением; перечня задач там нет. Он был бы третьим индексом и
|
||||
@@ -184,7 +190,7 @@ stateDiagram-v2
|
||||
- **Статус цели выводится.** Цель достигнута, когда у неё не осталось открытых
|
||||
задач; `[x]`/`[~]` руками не ведутся, а `close` цели с живыми задачами
|
||||
скрипт запретит. Достижение — `close <цель> --implemented`: файл удаляется,
|
||||
строка с датой переезжает в `готово`. Ошиблись — `reopen` вернёт файл и
|
||||
строка с датой переезжает в `Готово`. Ошиблись — `reopen` вернёт файл и
|
||||
**снимет строку достигнутого**, чтобы роадмап не утверждал того, чего нет. Единственная оговорка: цель без задач неотличима — «ещё не
|
||||
разобрана» или «всё закрыто». Различает **тег `decomposed`** в мете
|
||||
цели: он ставится, когда цель разложена на задачи. Тег, а не строка в теле —
|
||||
@@ -248,8 +254,34 @@ stateDiagram-v2
|
||||
|
||||
## Как написана задача
|
||||
|
||||
Два требования к тексту, и оба про то, чтобы задачу можно было **оценить, не
|
||||
открывая код**.
|
||||
Три требования к тексту. Первое — про заголовок, два остальных про то, чтобы
|
||||
задачу можно было **оценить, не открывая код**.
|
||||
|
||||
**Заголовок отвечает на вопрос своего типа.** Вопросов три, поэтому и форм три:
|
||||
|
||||
| Тип | Отвечает на | Пример |
|
||||
| --- | --- | --- |
|
||||
| цель | что приложение будет уметь | Соперником может быть компьютер |
|
||||
| задача | что нужно сделать | Печатать поле одним куском кода |
|
||||
| идея | о чём она | Подсказка следующего хода |
|
||||
|
||||
Задача — **глаголом в неопределённой форме**, перед ним допускается «не»: «Не
|
||||
отбрасывать молча лишние символы в ходе», а не «Лишние символы молча
|
||||
отбрасываются». Описательный заголовок называет **состояние**, а из состояния не
|
||||
видно, чего от работы ждут: «Ничья объявляется, пока клетки есть» одинаково
|
||||
читается и как жалоба, и как задание, — и в списке, где решают «брать или не
|
||||
брать», это разные вещи. Идея формы действия не несёт **намеренно**: что делать,
|
||||
ещё неизвестно, и заголовок-действие обещал бы решённость, которой нет.
|
||||
|
||||
Из этого же правила растёт разница индексов: роадмап — список возможностей,
|
||||
беклог — список работ, и если заголовки перепутать формами, каждый из них
|
||||
начинает читаться как другой.
|
||||
|
||||
`check` считает заголовки не в форме действия и печатает **число** в блоке
|
||||
здоровья, не замечанием на файл: проверка эвристическая (первое слово на
|
||||
`-ть`/`-ти`/`-чь`), а беклог, заведённый до правила, не переоформляют «заодно».
|
||||
Годность формулировки — не машине: её смотрит
|
||||
[агент вычитки](#вычитка-формулировок).
|
||||
|
||||
**Функции и границы, а не намерения.** Задача называет, что система начнёт
|
||||
делать, и какие границы это трогает: эндпоинт или команду, таблицу и миграцию,
|
||||
@@ -428,6 +460,28 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
обе половины остаются в одной ступени, потому что несокращаемый костяк проверок
|
||||
платится за каждую задачу отдельно.
|
||||
|
||||
### Вычитка формулировок
|
||||
|
||||
Язык записей судит **отдельный проход** — агент `task-wording`, а не тот же
|
||||
агент, который их только что написал: самопроверка текста слабее всего ровно
|
||||
там, где формулировка казалась удачной при написании.
|
||||
|
||||
Зовётся он **пачкой, а не на каждую запись**: после заведения нескольких задач,
|
||||
после разбора находок ревью и на переоценке. Ему передаётся список файлов и —
|
||||
если есть — паспорт, архитектура и конвенции проекта: по ним он отличает
|
||||
неизвестный термин от известного.
|
||||
|
||||
Он ничего не правит. Возвращает готовые формулировки, и их подставляет скилл:
|
||||
заголовок — `edit <слаг> --title …`, «зачем» — `edit <слаг> --why …`, остальное
|
||||
редактором. **Заголовок и «зачем» — это то, по чему задачу выбирают, поэтому
|
||||
менять их молча нельзя**: покажи предложенное пользователю вместе с тем, что
|
||||
было. Правки в теле (границы, критерии, язык) применяются сразу.
|
||||
|
||||
Что он смотрит и чего не смотрит — в его уставе; коротко: форму заголовка по
|
||||
типу записи, «зачем» вместо пересказа, англицизмы, неизвестные термины, границы
|
||||
вместо замысла, годность оракулов, предписания процесса. Всё, что ловит
|
||||
`tasks.py check`, он не трогает намеренно.
|
||||
|
||||
### Гигиена полей
|
||||
|
||||
Правится по ходу любой операции, которая задачи касается (но не «заодно» по
|
||||
@@ -479,7 +533,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
канон, а не по одному на каталог. Неизвестный ключ — код 3 на любой команде,
|
||||
так что лишнее слово в этом объекте останавливает работу с задачами целиком.
|
||||
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
|
||||
и названия — дело проекта (умолчание `ядро` / `инфра`). **В конфиге их нет** —
|
||||
и названия — дело проекта (умолчание `Ядро` / `Инфра`). **В конфиге их нет** —
|
||||
второй список разошёлся бы с заголовками молча.
|
||||
|
||||
### Вызов из другого плагина
|
||||
|
||||
@@ -63,7 +63,7 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
|
||||
1. **Осмотрись.** Где лежат задачи, роадмап, заметки. Каталог задач по канону —
|
||||
всегда `docs/tasks`. Секции беклога (`--sections`) — по умолчанию
|
||||
`ядро,инфра`; если у проекта деление другое по существу, оно называется
|
||||
`Ядро,Инфра`; если у проекта деление другое по существу, оно называется
|
||||
здесь, а не подгоняется под умолчание, и становится **заголовками `##`
|
||||
индекса** — их единственным домом. В `docs/.pm.json` секции не пишутся.
|
||||
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
Цель обязательна у находки, которая оказалась **новой возможностью**
|
||||
(`kind:feature`): нашлось поведение, которого никто не заказывал, и его надо
|
||||
либо заказать целью, либо убрать. Подходящей цели нет — заведи её
|
||||
(`add --type goal --section направления`) в том же проходе.
|
||||
(`add --type goal --section Направления`) в том же проходе.
|
||||
5. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в
|
||||
пакетный файл / уже заведено / отброшено, и под какую цель — пачкой через
|
||||
`AskUserQuestion`. Это тот же барьер, что и «три кандидата» в интейке из
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
```markdown
|
||||
# Тай-брейк при равной полноте
|
||||
|
||||
- **Секция:** ядро — вышла из спринта: остаток писал нерешённое в журнал
|
||||
- **Секция:** Ядро — вышла из спринта: остаток писал нерешённое в журнал
|
||||
- **Зачем:** порядок канонических форм берёт меньшее в 96% случаев — для накопительных это систематический недосчёт
|
||||
- **Теги:** goal:merge-robustness, kind:fix, sprint:2026-08-03
|
||||
|
||||
@@ -40,6 +40,12 @@
|
||||
префиксом `[goal]` / `[idea]`; обычная задача — без префикса.
|
||||
Отдельного поля типа **нет**: два места для одного факта разъезжаются, а
|
||||
префикс виден прямо в индексе, где и принимается решение «брать или не брать».
|
||||
- **Форма заголовка — по типу записи.** Задача отвечает на «что нужно сделать»
|
||||
и пишется глаголом в неопределённой форме («Печатать поле одним куском кода»,
|
||||
«Не отбрасывать молча лишние символы»); цель — на «что приложение будет
|
||||
уметь»; идея просто называет, о чём она. Почему так — SKILL.md, «Как написана
|
||||
задача». `check` считает заголовки не в форме действия и печатает число в
|
||||
здоровье; годность формулировки смотрит агент `task-wording`.
|
||||
- **Мета-блок** — список сразу после заголовка, **поле на строку**. Обязательна
|
||||
секция, причина после тире желательна (именно она объясняет, почему задача
|
||||
здесь оказалась — в том числе «вышла из спринта: …»), «зачем» и теги
|
||||
@@ -159,7 +165,7 @@
|
||||
```markdown
|
||||
# [goal] Исход слияния не зависит от порядка доставки
|
||||
|
||||
- **Секция:** направления
|
||||
- **Секция:** Направления
|
||||
- **Теги:** decomposed
|
||||
|
||||
Ради чего: точки из разных доставок сходятся в один часовой объект, и сегодня
|
||||
@@ -187,7 +193,7 @@
|
||||
которой задачи есть, а цель с тегом и без задач — прямое приглашение закрыть.
|
||||
- Цель живёт в `ROADMAP.md` и **никогда** — в `BACKLOG.md` или `SPRINT.md`.
|
||||
- **Достигнутая цель не исчезает.** `close <слаг> --implemented` удаляет файл и
|
||||
переносит строку в секцию `готово` с датой:
|
||||
переносит строку в секцию `Готово` с датой:
|
||||
`- 2026-08-04 \`merge-order\` — Исход слияния не зависит от порядка доставки. …`
|
||||
Ссылки на файл в ней нет — файл удалён, а битая ссылка это ошибка `check`.
|
||||
Поведение живёт в спеках проекта; роадмап отвечает, **когда и в каком порядке**
|
||||
@@ -219,8 +225,8 @@
|
||||
|
||||
| Файл | Что отвечает | Секции |
|
||||
| --- | --- | --- |
|
||||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические: `готово`, `запланировано`, `направления`, `разработка` (англ. `done`, `planned`, `directions`, `tooling`) |
|
||||
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию ядро/инфра) |
|
||||
| `ROADMAP.md` | что приложение уже умеет и чего ещё не умеет | канонические: `Готово`, `Запланировано`, `Направления`, `Разработка` (англ. `Done`, `Planned`, `Directions`, `Tooling`) |
|
||||
| `BACKLOG.md` | что **можно взять** — только задачи | секции проекта (по умолчанию Ядро/Инфра) |
|
||||
| `SPRINT.md` | какая цель и какой набор под неё | одна: «Набор» |
|
||||
| `REJECTED.md` | что ушло без реализации и почему | — |
|
||||
|
||||
@@ -239,14 +245,20 @@
|
||||
Постоянно пустая секция со старой семантикой «разбираются пачками» противоречила
|
||||
бы правилу «эскалируем немедленно», поэтому `init` её не заводит, а `check`
|
||||
говорит о ней в чужом беклоге. Переезжаешь с такой секцией — удали её. В секции
|
||||
**`запланировано`** очередь значима и обосновывается прозой; двигают строку
|
||||
`move <slug> --section запланировано --after <другой>`. В секции **`готово`**
|
||||
**`Запланировано`** очередь значима и обосновывается прозой; двигают строку
|
||||
`move <slug> --section Запланировано --after <другой>`. В секции **`Готово`**
|
||||
строки не той формы, что у прочих индексов: дата, слаг, заголовок — как в
|
||||
`REJECTED.md`, и по той же причине (файла уже нет, ссылаться некуда).
|
||||
|
||||
**Секции роадмапа закреплены** — состав, полнота и единство языка проверяются
|
||||
`check`; секции беклога проект называет сам. Почему так — SKILL.md.
|
||||
|
||||
**Заголовок секции пишется с прописной, и после него идёт пустая строка** — во
|
||||
всех индексах, включая секции беклога, имена которых выбирает проект. Написание
|
||||
канонических секций и отбивку правит `check --fix`; он же сводит написание
|
||||
секции в мете файла с заголовком индекса — **имя секции принадлежит заголовку**,
|
||||
файл на неё лишь ссылается, и принадлежность сверяется по нижнему регистру.
|
||||
|
||||
Индексы **производны**: расходятся с файлом — правим индексы (`check --fix`).
|
||||
Строку руками не пишут.
|
||||
|
||||
@@ -269,7 +281,7 @@
|
||||
```markdown
|
||||
- 2026-07-23 `versii-kachestvo-repaki` — Версии и качество одного тайтла.
|
||||
Причина: калибровка болей — не боль, ни разу не возникло за полгода.
|
||||
Была секция: инфра.
|
||||
Была секция: Инфра.
|
||||
```
|
||||
|
||||
Реализованные сюда не попадают: у них остаётся коммит и документация. У
|
||||
|
||||
@@ -127,7 +127,7 @@ DEFAULTS = {
|
||||
# с диском первым делом, иначе кривой ключ выглядит как пропавший файл).
|
||||
PATH_KEYS = ("items", "backlog", "roadmap", "sprint", "rejected")
|
||||
|
||||
DEFAULT_SECTIONS = "ядро,инфра"
|
||||
DEFAULT_SECTIONS = "Ядро,Инфра"
|
||||
|
||||
# Секции роадмапа **канонические**, в отличие от секций беклога. Причина не в
|
||||
# любви к единообразию: у каждой своя семантика — достигнутое, очередь, долгие
|
||||
@@ -136,12 +136,14 @@ DEFAULT_SECTIONS = "ядро,инфра"
|
||||
# не несут, это полки, и остаются делом проекта.
|
||||
#
|
||||
# Пара на секцию: русское имя и английское. Проект держит **один язык на весь
|
||||
# индекс** — вперемешку это дрейф, который check называет вслух.
|
||||
# индекс** — вперемешку это дрейф, который check называет вслух. Сверка везде
|
||||
# идёт по нижнему регистру, а пишется — как здесь: заголовок предложением, с
|
||||
# прописной.
|
||||
ROADMAP_SECTIONS = (
|
||||
("готово", "done"), # достигнутое: что приложение уже умеет
|
||||
("запланировано", "planned"), # очередь значима, обоснована прозой
|
||||
("направления", "directions"), # очереди нет, тянутся долго
|
||||
("разработка", "tooling"), # инструмент и процесс, не про приложение
|
||||
("Готово", "Done"), # достигнутое: что приложение уже умеет
|
||||
("Запланировано", "Planned"), # очередь значима, обоснована прозой
|
||||
("Направления", "Directions"), # очереди нет, тянутся долго
|
||||
("Разработка", "Tooling"), # инструмент и процесс, не про приложение
|
||||
)
|
||||
ACHIEVED, PLANNED = 0, 1 # индексы в ROADMAP_SECTIONS
|
||||
DEFAULT_ROADMAP_SECTIONS = ",".join(ru for ru, _ in ROADMAP_SECTIONS)
|
||||
@@ -157,6 +159,12 @@ META_FIELD = re.compile(r"^\*\*(.+?):\*\*\s*(.*)$")
|
||||
# сами; пишется всегда новое.
|
||||
WHY_KEYS = ("зачем", "why", "хук", "hook")
|
||||
|
||||
# Ключи, которые скрипт у себя признаёт. Нужны не разбору (там ключи
|
||||
# перечислены по месту), а поиску поля, отбившегося от блока: сверять с
|
||||
# закрытым списком — единственный способ не спутать поле меты со строкой тела
|
||||
# вида `- **Важно:** …`.
|
||||
META_KEYS = {"секция", "section", "теги", "tags", *WHY_KEYS}
|
||||
|
||||
|
||||
def meta_span(lines: list[str]) -> tuple[int, int] | None:
|
||||
"""Границы мета-блока — `[начало, конец)`. None, если меты нет.
|
||||
@@ -339,7 +347,7 @@ class Plan:
|
||||
self.writes.append((path, text))
|
||||
|
||||
def index(self, lay: "Layout", kind: str, lines: list[str]) -> None:
|
||||
self.file(lay.index(kind), "\n".join(lines))
|
||||
self.file(lay.index(kind), "\n".join(spaced_sections(lines)))
|
||||
|
||||
def delete(self, path: Path) -> None:
|
||||
self.deletes.append(path)
|
||||
@@ -538,6 +546,42 @@ def parse_entries(lines: list[str]) -> tuple[dict[str, dict], list[str]]:
|
||||
return entries, sections
|
||||
|
||||
|
||||
INFINITIVE = re.compile(r"(?:ть|ти|чь)(?:ся)?$")
|
||||
|
||||
|
||||
def action_title(title: str) -> bool:
|
||||
"""Заголовок задачи в форме действия: первое слово — глагол в неопределённой
|
||||
форме, перед ним допускается «не».
|
||||
|
||||
Эвристика, и намеренно грубая: русская морфология без словаря не разбирается,
|
||||
а «Часть данных теряется» от «Печатать поле» отличается ровно окончанием
|
||||
первого слова. Поэтому результат идёт **счётчиком в здоровье**, а не
|
||||
замечанием: ошибиться на одном заголовке дешевле, чем не заметить двадцати.
|
||||
"""
|
||||
words = re.findall(r"[^\W\d_]+", title)
|
||||
if not words:
|
||||
return False
|
||||
first = words[0].lower()
|
||||
if first in ("не", "не-") and len(words) > 1:
|
||||
first = words[1].lower()
|
||||
return bool(INFINITIVE.search(first))
|
||||
|
||||
|
||||
def spaced_sections(lines: list[str]) -> list[str]:
|
||||
"""Отбивка после заголовка секции. Заголовок, пустая строка, потом
|
||||
содержимое — во всех индексах одинаково.
|
||||
|
||||
Живёт на записи, а не на вставке: через `Plan.index` проходит **каждая**
|
||||
запись индекса, и чинить отбивку в каждом месте вставки значило бы
|
||||
полагаться на то, что ни одно из них не забыли."""
|
||||
out: list[str] = []
|
||||
for i, line in enumerate(lines):
|
||||
out.append(line)
|
||||
if SECTION.match(line) and i + 1 < len(lines) and lines[i + 1].strip():
|
||||
out.append("")
|
||||
return out
|
||||
|
||||
|
||||
def index_lint(lines: list[str], label: str) -> list[str]:
|
||||
"""Структурные дефекты индекса, которых схлопнутый dict не видит: битые
|
||||
строки-пункты, дубли на один файл, задачи до первой секции."""
|
||||
@@ -547,6 +591,9 @@ def index_lint(lines: list[str], label: str) -> list[str]:
|
||||
for num, line in enumerate(lines, 1):
|
||||
if (m := SECTION.match(line)):
|
||||
section = m.group(1)
|
||||
if num < len(lines) and lines[num].strip():
|
||||
errors.append(f"{label}:{num}: после заголовка «{section}» нет"
|
||||
f" пустой строки; починит `check --fix`")
|
||||
continue
|
||||
if not line.startswith("- ["):
|
||||
continue
|
||||
@@ -597,14 +644,15 @@ def parse_task(path: Path) -> dict:
|
||||
rtype, bare = m.group(1).strip().lower(), m.group(2).strip()
|
||||
# Мета — блок под заголовком (task-format.md). Порядок полей свободный:
|
||||
# секция распознаётся, где бы она ни стояла.
|
||||
section, reason, why, tags, legacy = "", "", "", [], False
|
||||
section, section_raw, reason, why, tags, legacy = "", "", "", "", [], False
|
||||
if (span := meta_span(lines)):
|
||||
legacy = meta_legacy(lines, span)
|
||||
for key, value in meta_fields(lines, span):
|
||||
key = key.lower()
|
||||
if key in ("секция", "section"):
|
||||
section, _, reason = (p.strip() for p in value.partition("—"))
|
||||
section = section.rstrip(".,").lower()
|
||||
section = section.rstrip(".,")
|
||||
section_raw, section = section, section.lower()
|
||||
elif key in WHY_KEYS:
|
||||
why = value
|
||||
elif key in ("теги", "tags"):
|
||||
@@ -612,11 +660,27 @@ def parse_task(path: Path) -> dict:
|
||||
goal = next((t[len(GOAL_TAG):] for t in tags if t.startswith(GOAL_TAG)), "")
|
||||
kind = next((t[len(KIND_TAG):] for t in tags if t.startswith(KIND_TAG)), "")
|
||||
return {"title": title, "bare": bare, "type": rtype, "section": section,
|
||||
"section_raw": section_raw,
|
||||
"reason": reason, "why": why, "tags": tags, "goal": goal, "kind": kind,
|
||||
"path": path,
|
||||
"path": path, "stray_meta": stray_meta(lines, span),
|
||||
"legacy_meta": legacy, "text": text, "body": body_sections(text)}
|
||||
|
||||
|
||||
def stray_meta(lines: list[str], span: tuple[int, int] | None) -> list[str]:
|
||||
"""Поля меты, оставшиеся за пределами блока. Так выглядит мета, разорванная
|
||||
пустой строкой: разбор дочитывает блок до разрыва, а всё, что ниже,
|
||||
становится телом — и поля теряются молча. Ловить обязательно: молчаливая
|
||||
потеря «зачем» или рода работы неотличима от того, что их не задавали, а
|
||||
следующий `edit` допишет второе такое же поле в мету."""
|
||||
if span is None:
|
||||
return []
|
||||
out = []
|
||||
for j in range(span[1], len(lines)):
|
||||
if (m := META_ITEM.match(lines[j].strip())) and m.group(1).strip().lower() in META_KEYS:
|
||||
out.append(m.group(1).strip())
|
||||
return out
|
||||
|
||||
|
||||
def tasks_of(lay: Layout) -> dict[str, dict]:
|
||||
if not lay.items.is_dir():
|
||||
return {}
|
||||
@@ -836,6 +900,12 @@ def check(lay: Layout, fix: bool = False) -> int:
|
||||
if task["legacy_meta"]:
|
||||
errors.append(f"{name}: мета одной строкой — старая форма;"
|
||||
f" `check --fix` перепишет её списком")
|
||||
if task["stray_meta"]:
|
||||
errors.append(f"{name}: поле меты в теле"
|
||||
f" ({', '.join(task['stray_meta'])}) — мета разорвана"
|
||||
f" пустой строкой, и всё, что ниже разрыва, потеряно."
|
||||
f" Убери пустую строку внутри блока; `--fix` этого не"
|
||||
f" делает: какое из двух значений верное, знает человек")
|
||||
if not task["section"]:
|
||||
errors.append(f"{name}: нет поля **Секция:** в мета-блоке")
|
||||
elif task["section"] not in known[home]:
|
||||
@@ -1018,6 +1088,17 @@ def health(lay: Layout, tasks: dict, entries: dict, sections: dict) -> None:
|
||||
print(f" с открытым вопросом: {len(questions)}"
|
||||
f" — в спринт не берутся, разбор первым шагом сессии")
|
||||
|
||||
# Форма заголовка — счётчиком, а не замечанием на файл. Правило верное, но
|
||||
# проверка эвристическая, а беклог, заведённый до правила, переоформляют не
|
||||
# «заодно»: десятки одинаковых замечаний научили бы пропускать весь блок.
|
||||
flat = sorted(n[:-3] for n, t in tasks.items()
|
||||
if t["type"] in TAKEABLE and not action_title(t["bare"]))
|
||||
if flat:
|
||||
print(f" заголовков не в форме действия: {len(flat)}"
|
||||
f" ({', '.join(flat[:5])}{', …' if len(flat) > 5 else ''})"
|
||||
f" — задача отвечает на «что нужно сделать»:"
|
||||
f" «Печатать поле одним куском», а не «Поле печатается одним куском»")
|
||||
|
||||
goals = {n[:-3]: t for n, t in tasks.items() if t["type"] == GOAL}
|
||||
if goals:
|
||||
counts = {g: sum(1 for t in tasks.values() if t["goal"] == g) for g in goals}
|
||||
@@ -1152,7 +1233,11 @@ def roadmap_lint(lines: list[str], label: str) -> list[str]:
|
||||
f" закреплены; прозаический заголовок здесь — секция,"
|
||||
f" в которую может уехать цель")
|
||||
continue
|
||||
langs.add(0 if section.lower() == ROADMAP_SECTIONS[i][0] else 1)
|
||||
langs.add(0 if section.lower() == ROADMAP_SECTIONS[i][0].lower() else 1)
|
||||
if section not in ROADMAP_SECTIONS[i]:
|
||||
errors.append(f"{label}: секция «{section}» написана не как в каноне"
|
||||
f" ({' | '.join(ROADMAP_SECTIONS[i])}) — заголовок"
|
||||
f" пишется с прописной; починит `check --fix`")
|
||||
if i in seen:
|
||||
errors.append(f"{label}: секция «{section}» повторяет «{seen[i]}» —"
|
||||
f" это одна и та же секция на двух языках")
|
||||
@@ -1207,9 +1292,10 @@ def insert_entry(lines: list[str], section: str, entry: str,
|
||||
raise Usage(f"секции «{section}» в индексе нет")
|
||||
end = next((j for j in range(hi + 1, len(lines)) if SECTION.match(lines[j])), len(lines))
|
||||
if first:
|
||||
ins = hi + 1
|
||||
while ins < end and not lines[ins].strip():
|
||||
ins += 1
|
||||
# Пустые строки после заголовка пропускаются, но только если за ними
|
||||
# что-то есть: у пустой секции пропускать нечего, и строка, вставленная
|
||||
# в её конец, съела бы отбивку перед следующим заголовком.
|
||||
ins = next((j for j in range(hi + 1, end) if lines[j].strip()), hi + 1)
|
||||
elif after:
|
||||
ai = find_entry_index(lines[hi:end], after)
|
||||
if ai is None:
|
||||
@@ -2096,6 +2182,50 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
|
||||
files[task["path"]] = upd
|
||||
fixed.append(f"{name}: проставлен тег «{DECOMPOSED_TAG}» — у цели есть задачи")
|
||||
|
||||
# 5. Форма индексов: канонический регистр секций роадмапа и отбивка после
|
||||
# заголовков. Регистр правится только у **канонических** секций: имена
|
||||
# секций беклога — дело проекта, и подгонять их под свой вкус скрипт
|
||||
# права не имеет.
|
||||
canon = {n.lower(): pair for pair in ROADMAP_SECTIONS for n in pair}
|
||||
for kind, lines in idx.items():
|
||||
if kind == "roadmap":
|
||||
for j, line in enumerate(lines):
|
||||
if not (m := SECTION.match(line)):
|
||||
continue
|
||||
pair = canon.get(m.group(1).lower())
|
||||
if pair is None or m.group(1) in pair:
|
||||
continue
|
||||
want = pair[0] if m.group(1).lower() == pair[0].lower() else pair[1]
|
||||
lines[j] = f"## {want}"
|
||||
fixed.append(f"{lay.name(kind)}: секция «{m.group(1)}» → «{want}»")
|
||||
dirty.add(kind)
|
||||
if spaced_sections(lines) != lines:
|
||||
fixed.append(f"{lay.name(kind)}: отбивка после заголовков секций")
|
||||
dirty.add(kind)
|
||||
|
||||
# 6. Написание секции в мете. Имя секции принадлежит **заголовку индекса** —
|
||||
# файл на секцию только ссылается, а принадлежность сверяется по нижнему
|
||||
# регистру. Поэтому расхождение в одном регистре однозначно: побеждает
|
||||
# заголовок. Без этого шага переезд на канон оставил бы «Готово» в
|
||||
# роадмапе и «готово» в каждом файле цели.
|
||||
for name, task in tasks.items():
|
||||
kind = home_index(task)
|
||||
if not task["section_raw"] or kind not in idx:
|
||||
continue
|
||||
_, heading = find_section(idx[kind], task["section"])
|
||||
if not heading or heading == task["section_raw"]:
|
||||
continue
|
||||
# Правим уже отложенный текст, если файл трогали выше: перечитать его с
|
||||
# диска значило бы стереть проставленный шагом 4 тег.
|
||||
staged = files.get(task["path"])
|
||||
src = (staged.splitlines() if staged is not None
|
||||
else task["path"].read_text(encoding="utf-8").splitlines())
|
||||
rebuilt = meta_rebuilt(src, section=heading)
|
||||
if rebuilt is None:
|
||||
continue
|
||||
files[task["path"]] = "\n".join(rebuilt) + "\n"
|
||||
fixed.append(f"{name}: секция в мете «{task['section_raw']}» → «{heading}»")
|
||||
|
||||
plan = Plan()
|
||||
for path, text in files.items():
|
||||
plan.file(path, text)
|
||||
@@ -2145,14 +2275,14 @@ def init_files(lay: Layout, sections: list[str], roadmap_sections: list[str],
|
||||
f"- **{ROADMAP_SECTIONS[ACHIEVED][0]}** — достигнутое: строку пишет\n"
|
||||
" `tasks.py close <цель> --implemented`, ссылки на файл в ней нет —\n"
|
||||
" файл удаляется, поведение живёт в спеках;\n"
|
||||
"- **запланировано** — очередь значима и обосновывается прозой;\n"
|
||||
"- **направления** — очереди нет, тянутся долго;\n"
|
||||
"- **разработка** — инструмент и процесс, не возможности приложения.\n"
|
||||
" Отдельно, чтобы не читаться как обещание продукта.\n\n"
|
||||
f"- **{ROADMAP_SECTIONS[PLANNED][0]}** — очередь значима и обосновывается прозой;\n"
|
||||
f"- **{ROADMAP_SECTIONS[2][0]}** — очереди нет, тянутся долго;\n"
|
||||
f"- **{ROADMAP_SECTIONS[3][0]}** — инструмент и процесс, не возможности\n"
|
||||
" приложения. Отдельно, чтобы не читаться как обещание продукта.\n\n"
|
||||
"Секции **канонические** и переименованию проектом не подлежат:\n"
|
||||
"у каждой свой смысл, и в первую пишет сам `close`. Английский\n"
|
||||
"вариант — done | planned | directions | tooling, один язык на весь\n"
|
||||
"индекс.\n\n"
|
||||
f"вариант — {' | '.join(pair[1] for pair in ROADMAP_SECTIONS)},"
|
||||
" один язык на весь\nиндекс.\n\n"
|
||||
+ "".join(f"## {s}\n\n" for s in roadmap_sections))
|
||||
out[lay.index("sprint")] = empty_sprint(lay)
|
||||
out[lay.index("rejected")] = (
|
||||
|
||||
Reference in New Issue
Block a user