diff --git a/av-dev-code/agents/review-basics.md b/av-dev-code/agents/review-basics.md index 7b69084..4c5176f 100644 --- a/av-dev-code/agents/review-basics.md +++ b/av-dev-code/agents/review-basics.md @@ -146,8 +146,8 @@ color: yellow **Молча отменённое решение ADR больше не проверяет никто, и это сознательно.** Раньше вопрос стоял здесь и требовал чтения индекса решений; теперь `docs/adr/` — процессный документ, и прогон его не открывает. Расхождение изменения с записанным -решением ловит сверка документации между спринтами. Строка об этом обязательна в -твоих границах покрытия. +решением ловит сверка документации — скилл `av-dev-docs:healthcheck`. Строка об +этом обязательна в твоих границах покрытия. **Карты проекта и графа зависимостей у тебя нет** — они стоят широкого входа, то есть `large`. Твой вход — **дифф и его окрестности**. Греп по базе тебе разрешён diff --git a/av-dev-code/agents/review-triage.md b/av-dev-code/agents/review-triage.md index b59fbc3..dfc9ddf 100644 --- a/av-dev-code/agents/review-triage.md +++ b/av-dev-code/agents/review-triage.md @@ -207,7 +207,7 @@ severity: 1. **Решения проекта не сверялись.** `docs/adr.*` — процессный документ, прогон его не открывает. Расхождение изменения с записанным решением ловит сверка - документации между спринтами, а не ревью. + документации — скилл `av-dev-docs:healthcheck`, а не ревью. 2. **Записанные наблюдения проекта не использовались.** `docs/research.*` — тоже процессный. Всякое число в находках снято проходом на этом прогоне; числа без приложенной команды замера в отчёте быть не должно. diff --git a/av-dev-code/skills/review/references/review-levels.md b/av-dev-code/skills/review/references/review-levels.md index d224b42..a188961 100644 --- a/av-dev-code/skills/review/references/review-levels.md +++ b/av-dev-code/skills/review/references/review-levels.md @@ -156,7 +156,7 @@ дёшев. Считается это по журналу дефектов и по отчётам, а не по ощущению: метка -напечатана в каждом отчёте, и посчитать её за спринт — работа на минуту. +напечатана в каждом отчёте, и посчитать её за месяц — работа на минуту. **У дрейфа вниз есть свой стимул, и его стоит назвать.** `small` дешевле по времени и по деньгам, а выбирает метку хоть и не автор, но проход, читающий diff --git a/av-dev-tasks/agents/task-form.md b/av-dev-tasks/agents/task-form.md index adb759f..6429126 100644 --- a/av-dev-tasks/agents/task-form.md +++ b/av-dev-tasks/agents/task-form.md @@ -1,6 +1,6 @@ --- name: task-form -description: "Проверка формы записи каталога задач по существу: тип, разошедшийся с содержанием записи, форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, разведка — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент task-wording. Использовать после заведения или разбора пачки записей, до взятия в спринт и на переоценке. Только чтение." +description: "Проверка формы записи каталога задач по существу: тип, разошедшийся с содержанием записи, форма заголовка по типу (цель — что приложение будет уметь, задача — что нужно сделать, разведка — о чём она), «зачем», пересказывающее заголовок вместо состояния и боли, раздел «Затрагивает» с замыслом вместо границ, критерий приёмки с оракулом только на словах, предписание процесса в теле, и связь задачи со строкой «Завершения» её цели. Читает файл цели, на которую ссылается задача. Отдаёт готовые формулировки на замену и ничего не правит сам. Язык текста (залог, оценки, стоп-слова, англицизмы) смотрит отдельный агент task-wording. Использовать после заведения или разбора пачки записей, при проверке готовности перед взятием в работу (tasks.py ready) и на груминге. Только чтение." tools: Read, Grep, Glob model: sonnet color: green @@ -130,12 +130,17 @@ color: green если ты открыл цель и увидел расхождение в самом документе, оно их. Увидел — назови в конце одной строкой, чтобы находка не пропала, но находкой не оформляй. -**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (наличие -разделов, число критериев, состав и написание секций, теги, тег `question` при -непустом разделе «Вопросы», согласованность индексов, битые ссылки, форма -заголовка как строки), **не пиши даже строкой**: это не потерянная находка, а -уже проверенное. Повторять машинную проверку словами — заводить второй дом для -одного правила. +**Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (состав и +написание секций, теги, тег `question` при непустом разделе «Вопросы», +согласованность индексов, битые ссылки, форма заголовка как строки), **не пиши +даже строкой**: это не потерянная находка, а уже проверенное. Повторять машинную +проверку словами — заводить второй дом для одного правила. + +**Наличие разделов и число критериев `check` поимённо не называет** — он считает +их строкой здоровья, а поимённо судит `tasks.py ready` на входе в работу. +Отсутствующий раздел сам по себе всё равно не твоя находка (её увидит `ready`); +твоя — раздел, который **есть и лжёт**: границы вместо замысла, критерий с +оракулом только на словах. **Содержание работы**: нужна ли задача, верно ли выбрана цель, не крупна ли она, достаточна ли декомпозиция. Седьмое правило подходит к этому близко и diff --git a/av-dev-tasks/agents/task-wording.md b/av-dev-tasks/agents/task-wording.md index 2acd539..3a2aa4e 100644 --- a/av-dev-tasks/agents/task-wording.md +++ b/av-dev-tasks/agents/task-wording.md @@ -191,11 +191,12 @@ color: green но находкой не оформляй. **Машинной проверке — вообще ничего.** Всё, что ловит `tasks.py check` (состав и -написание секций, наличие разделов своего типа, число критериев, теги, тег -`question` при непустом разделе «Вопросы», согласованность файлов с индексами, -битые ссылки), **не пиши даже строкой**: это не потерянная находка, а уже -проверенное. Повторять машинную проверку словами — заводить второй дом для -одного правила. +написание секций, теги, тег `question` при непустом разделе «Вопросы», +согласованность файлов с индексами, битые ссылки), **не пиши даже строкой**: это +не потерянная находка, а уже проверенное. Повторять машинную проверку словами — +заводить второй дом для одного правила. Наличие разделов своего типа и число +критериев `check` только считает — поимённо их судит `tasks.py ready`, и это +тоже не твоя находка: твоя — язык того, что уже написано. **Содержание работы**: нужна ли задача, верно ли выбрана цель, не крупна ли она, достаточна ли декомпозиция. Это разбор, и его ведёт человек со скиллом `tasks`. diff --git a/av-dev-tasks/skills/groom/references/portions.md b/av-dev-tasks/skills/groom/references/portions.md index a8b3401..c1773a3 100644 --- a/av-dev-tasks/skills/groom/references/portions.md +++ b/av-dev-tasks/skills/groom/references/portions.md @@ -112,10 +112,11 @@ Задача из верхних строк `--stale`, которую и этот заход оставляет без изменений, **либо двигается (меняет цель, поднимается в очереди, уходит с причиной), либо -остаётся с явно записанной причиной**, почему её держим (`move --section -<та же> --reason …`). Молчаливое «оставить как есть» на давно неподвижной задаче -— это решение не принимать решение; запись причины превращает его в осознанное и -не даёт тому же вопросу всплыть на следующем груминге. +остаётся с явно записанной причиной**, почему её держим (`move --reason +…` — без `--section` секция берётся текущая). Молчаливое «оставить как есть» на +давно неподвижной задаче — это решение не принимать решение; запись причины +превращает его в осознанное и не даёт тому же вопросу всплыть на следующем +груминге. ## Шаг 4. Что важно сейчас — расстановка diff --git a/av-dev-tasks/skills/tasks/SKILL.md b/av-dev-tasks/skills/tasks/SKILL.md index 12e5dbb..97680c2 100644 --- a/av-dev-tasks/skills/tasks/SKILL.md +++ b/av-dev-tasks/skills/tasks/SKILL.md @@ -383,7 +383,7 @@ python3 $tk check --dir D --fix # + починить дрейф (тип python3 $tk list --dir D [--stale] [--section S] [--type T] [--tag a,b] [--goal S] [--raw] [--index …] [--questions] python3 $tk add --dir D --slug S --title T --type goal|feature|fix|chore|research [--section S] [--goal G] [--why «зачем»] [--tag a,b] python3 $tk edit S --dir D [--title T] [--why «зачем»] [--type T] [--goal G] [--add-tag a,b] [--rm-tag c] -python3 $tk move S --dir D --section S [--reason R] [--after S | --first] +python3 $tk move S --dir D [--section S] [--reason R] [--after S | --first] # без --section — текущая секция python3 $tk close S --dir D --reason R # в REJECTED.md + удалить (ушла без реализации) python3 $tk close S --dir D --implemented # просто удалить (реализована и закоммичена) python3 $tk reopen S --dir D --reason R # вернуть закрытую: приёмка не сошлась @@ -459,8 +459,11 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап проставляет человек — `edit <слаг> --type …`. **Что механизировано, а что нет.** Схему типа проверяет `ready` на входе в -работу — там, где по ней принимают решение; `check` о недостающем только -напоминает счётчиком «готово к взятию». У каждой части своя глубина: +работу — там, где по ней принимают решение; `check` поимённо о ней не говорит, а +считает: строка здоровья **«схема типа не выполнена: N из M»** называет число и +первые слаги, строка **«готово к взятию»** — сколько задач беклога пройдут +`ready` целиком (схема плюс цель плюс отсутствие открытого вопроса). Это две +разные строки, и совпадение их чисел — совпадение. У каждой части своя глубина: - **тип** — жёстко: назван и из закрытого словаря; - **критерии приёмки** (`feature`, `fix`, `chore`) — число пунктов жёстко @@ -605,7 +608,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап мету файла и строку индекса заодно; - **вопрос, застрявший в прозе** — вынимается в раздел «Вопросы» плюс тег `question` (`edit --add-tag question`), иначе он не виден ни `list - --questions`, ни правилу «задача с открытым вопросом в набор не берётся»; + --questions`, ни правилу «задача с открытым вопросом в работу не берётся»; - **тег, который некому снять** — `question` после ответа снимается `edit --rm-tag question` вместе с записью ответа в тело **и опустошением раздела «Вопросы»**: судит раздел, а не тег (`references/task-format.md`); diff --git a/av-dev-tasks/skills/tasks/references/adopt.md b/av-dev-tasks/skills/tasks/references/adopt.md index 0d34b7d..55811ea 100644 --- a/av-dev-tasks/skills/tasks/references/adopt.md +++ b/av-dev-tasks/skills/tasks/references/adopt.md @@ -94,8 +94,9 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \ быть названо, иначе следующий агент примет пустой беклог за поломку. `apply` печатает состояние по факту: сколько задач без цели (это **ошибки** -`check`) и сколько без критериев (`check` их ошибкой не считает, но `ready` -такую задачу не пропустит). Закрывается это **порциями груминга** — скилл +`check`) и сколько не собрало разделы своего типа (для `check` это не ошибка, а +строка здоровья, но `ready` такую задачу не пропустит). Закрывается это +**порциями груминга** — скилл `groom`, 5–8 задач за порцию: проставить цели, превратить «готово, когда» в критерии с оракулами, вынуть вопросы из прозы в раздел «Вопросы». Там же беклогу впервые назначается **порядок**: после адаптации его нет вовсе, а diff --git a/av-dev-tasks/skills/tasks/references/from-review.md b/av-dev-tasks/skills/tasks/references/from-review.md index 44d5fe4..4032a13 100644 --- a/av-dev-tasks/skills/tasks/references/from-review.md +++ b/av-dev-tasks/skills/tasks/references/from-review.md @@ -73,21 +73,31 @@ Без него через месяц не отличить проверенную находку от догадки. 7. `tasks.py check`. -## Куда девается серьёзность, если приоритетов нет +## Куда девается серьёзность находки -Приоритетов нет, и отображать серьёзность некуда — но **выкидывать её нельзя**. -Правило замены: +Уровня серьёзности в записи нет — но **выкидывать её нельзя**: серьёзность +отображается **в позицию в очереди**, потому что приоритет и есть порядок строк +в беклоге (правило 4 [SKILL.md](../SKILL.md)). Отображается через довод, а не +напрямую: своей шкалы у интейка нет, доводы расстановки перечислены в +[скилле груминга](../../groom/SKILL.md#приоритет-как-его-расставляют), и +серьёзность попадает ровно в один из них. -- **тяжёлая находка со свидетельством** → задача под ту цель, которой она - угрожает, и **кандидат на верх очереди**: серьёзность здесь превращается в - довод при расстановке приоритета, а не в уровень в файле. Довод записывается - причиной в мете (`--reason`), иначе к моменту груминга его никто не вспомнит. - Саму строку интейк ставит в конец секции: очередь назначает человек; -- **находка, которая не ждёт груминга вовсе** (необратимый ущерб, сломан общий - станок), — не интейк: это работа прямо сейчас, а в беклог она падает, только - если ждать всё-таки можно; +- **тяжёлая находка со свидетельством о сломанном сейчас** → задача под ту цель, + которой она угрожает, и **первой строкой секции**: `move <слаг> --first + --reason «сломано сейчас: …»`. Это довод «что сломано сейчас» из перечня + груминга — единственный, который не требует сравнения с соседями по очереди, + потому что сломанное дорожает само. Позицию всё равно назначает человек, и + здесь он её уже назначил: верх очереди для такой находки предъявляется картой + шага 5, а не проставляется молча; +- **тяжёлая находка о риске, а не о поломке** (дорожает от ожидания, + разблокирует остальное) → в конец секции, а довод — причиной в мете + (`--reason`). Позицию назначит человек на ближайшем груминге, сравнив её с + верхом очереди; без записанного довода сравнивать он будет с нуля; +- **находка, которая не ждёт груминга вовсе** (необратимый ущерб, покраснела + проверка, которую проект назвал сломанным), — не интейк: это работа прямо + сейчас, а в беклог она падает, только если ждать всё-таки можно; - **низкая уверенность или нет свидетельства** → сырьё (`research` с пустым - разделом «Вопрос»); + разделом «Вопрос»): его место в очереди производно от типа — конец секции; - **мелочь** → строка в пакетный файл; - **уже починено / развилка решена сейчас** → ничего. diff --git a/av-dev-tasks/skills/tasks/references/split.md b/av-dev-tasks/skills/tasks/references/split.md index e48ef32..3ecaa94 100644 --- a/av-dev-tasks/skills/tasks/references/split.md +++ b/av-dev-tasks/skills/tasks/references/split.md @@ -57,9 +57,11 @@ `REJECTED.md` здесь — не «выкинули», а именно тот след, что переживает запись: через квартал вопрос «куда делась задача X» отвечается строкой со ссылками на наследников, а не археологией git; -- родитель осмыслен как **возможность**, а не как шаг → это цель. Тип на месте - не меняется (цель живёт в другом индексе): заводится `--type goal` в `ROADMAP.md`, - части получают `--goal <новый слаг>`, родитель закрывается с причиной-ссылкой. +- родитель осмыслен как **возможность**, а не как шаг → это цель, и **строка + переезжает**: `edit --type goal --section <часть роадмапа>` снимает её с + `BACKLOG.md` и вставляет в `ROADMAP.md`. Файл в `items/` при этом не двигается — + он и есть запись. Части получают `--goal <слаг родителя>`, а закрывать родителя + нечем и незачем: он не выкинут, он стал целью. **Промежуточного зонтика между целью и задачей нет.** Тип `epic` упразднён: роль зонтика играет цель, а слишком крупный шаг дробится на шаги помельче под @@ -70,7 +72,7 @@ Задача, которая **оказалась крупнее задачи**, распознаётся до того, как под неё заведено предложение об изменении: иначе его придётся выбрасывать. Она выходит -уходит на декомпозицию, а её строка возвращается в беклог с причиной +из работы на декомпозицию, а её строка возвращается в беклог с причиной (`move … --reason "крупнее задачи"`). Части заводятся сразу под той же целью, и **место в очереди им назначает человек**: машина поставит их в конец секции, а крупная задача редко распадается на что-то менее срочное, чем была сама. diff --git a/av-dev-tasks/skills/tasks/references/task-chore.md b/av-dev-tasks/skills/tasks/references/task-chore.md index a7d2095..c9156d9 100644 --- a/av-dev-tasks/skills/tasks/references/task-chore.md +++ b/av-dev-tasks/skills/tasks/references/task-chore.md @@ -48,13 +48,13 @@ 5. **Проверить, что это не «заодно».** Обслуживание любит склеиваться в пачку («обновить зависимости и переписать сборку и убрать мёртвый код»). Не мерджится порознь — это несколько задач ([split.md](split.md)). -6. **Цель не проставлять.** `chore` служит работоспособности, а не направлению, - работоспособности, а не направлению. Работа по сопровождению проекта - при этом видна в роадмапе — секцией `Сопровождение`, но целью не становится. +6. **Цель не проставлять.** `chore` служит работоспособности, а не направлению. + Работа по сопровождению проекта при этом видна в роадмапе — секцией + `Сопровождение`, но целью не становится. ## Что видит машина, а что человек -`check` и `ready` смотрят на **наличие непустого** `Затрагивает` и на +`ready` смотрит на **наличие непустого** `Затрагивает` и на **число** критериев — ровно то же, что у `feature`. Разница между типами здесь не в строгости проверки, а в том, **кому адресован ответ** на «что станет наблюдаемо иначе», — и это судит человек. diff --git a/av-dev-tasks/skills/tasks/references/task-feature.md b/av-dev-tasks/skills/tasks/references/task-feature.md index df92abd..043ff9d 100644 --- a/av-dev-tasks/skills/tasks/references/task-feature.md +++ b/av-dev-tasks/skills/tasks/references/task-feature.md @@ -48,9 +48,11 @@ ## Что видит машина, а что человек -`check` и `ready` смотрят на **наличие непустого** раздела `Затрагивает`, -на **число** критериев (меньше двух — отказ, больше пяти — замечание) и на цель. -Наличие оракула проверяется **эвристикой** — словом «оракул» в пункте. +Схему типа судит `ready` на входе в работу: **наличие непустого** раздела +`Затрагивает`, **число** критериев (меньше двух — отказ, больше пяти — +замечание) и цель. Наличие оракула проверяется **эвристикой** — словом «оракул» +в пункте. `check` этого поимённо не говорит, а считает строкой здоровья +(`SKILL.md`, «Что механизировано, а что нет»). Полнота перечня границ машине не видна: границу, которую забыли назвать, она от отсутствующей не отличает. Настоящий оракул от слова «оракул» тоже не отличает. diff --git a/av-dev-tasks/skills/tasks/references/task-fix.md b/av-dev-tasks/skills/tasks/references/task-fix.md index 15b0011..3a9871b 100644 --- a/av-dev-tasks/skills/tasks/references/task-fix.md +++ b/av-dev-tasks/skills/tasks/references/task-fix.md @@ -54,9 +54,8 @@ почти всегда есть парный критерий: **прежнее поведение не сломалось** («ввод `а1` принимается по-прежнему»). Без него починка чинит одно и ломает соседнее. -6. **Цель не выдумывать.** `fix` служит работоспособности, а не направлению, и в - работоспособности, а не направлению. Придуманная цель — то же враньё, от - которого спасает тип. +6. **Цель не выдумывать.** `fix` служит работоспособности, а не направлению. + Придуманная цель — то же враньё, от которого спасает тип. 7. **Записать дефект в журнал** `docs/review.md` с пометкой «проскочил / пойман ревью». Проскочившие — проверочный набор для калибровки конвейера; пойманные с оракулом — лучшая опора для прохода ревью: проектные, воспроизводимые, @@ -64,7 +63,7 @@ ## Что видит машина, а что человек -`check` и `ready` смотрят на **наличие непустого** `Воспроизведения` и +`ready` смотрит на **наличие непустого** `Воспроизведения` и `Затрагивает` и на **число** критериев. Годность воспроизведения — человеку: шаги, по которым ничего не воспроизводится, машина от годных не отличает, и делать вид, что проверено больше проверенного, хуже, чем не проверять вовсе. diff --git a/av-dev-tasks/skills/tasks/references/task-format.md b/av-dev-tasks/skills/tasks/references/task-format.md index 788a235..05f6f4e 100644 --- a/av-dev-tasks/skills/tasks/references/task-format.md +++ b/av-dev-tasks/skills/tasks/references/task-format.md @@ -142,7 +142,7 @@ имя таблицы стабильно, номер последней миграции протухает молча. Пишется `таблица points и её миграция`, а не `миграция 0042`. -**Что из этого механизировано.** `check` и `ready` смотрят только на +**Что из этого механизировано.** `ready` смотрит только на **наличие непустого раздела**. Полнота перечня машине не видна: границу, которую забыли назвать, она от отсутствующей не отличает. Раздела нет — отказ во взятии: оценивать нечем. @@ -158,7 +158,7 @@ конкретизация вопроса «по чему видно, что закончено» из теста готовности ниже: там сказано «признак завершённости», здесь — «признак плюс чем проверяется». -**Что из этого механизировано.** `check` и `ready` считают пункты: меньше +**Что из этого механизировано.** `ready` считает пункты: меньше двух — отказ («— работает» одной строкой больше не проходит), больше пяти — замечание, обычно это признак, что задача крупнее задачи. Наличие оракула проверяется **эвристикой** — словом «оракул» в пункте, — и потому даёт только diff --git a/av-dev-tasks/skills/tasks/references/task-goal.md b/av-dev-tasks/skills/tasks/references/task-goal.md index e0170bf..44dd551 100644 --- a/av-dev-tasks/skills/tasks/references/task-goal.md +++ b/av-dev-tasks/skills/tasks/references/task-goal.md @@ -75,9 +75,9 @@ не попадает: `Готово` отвечает «что приложение умеет», а отменённая цель не умеет ничего. -**Место этому — переоценка на сессии, а не отдельный заход.** Отмена цели значит -разбор всех её задач, а разбор задач и есть шаг 3 сессии -(скилл `groom`, разбор «что перестало быть важным»). Отменять на ходу, +**Место этому — груминг, а не отдельный заход.** Отмена цели значит +разбор всех её задач, а разбор задач и есть шаг 3 груминга +(скилл `groom`, «что перестало быть важным»). Отменять на ходу, между делом, — верный способ закрыть скопом то, что стоило перевесить. ## Что видит машина, а что человек diff --git a/av-dev-tasks/skills/tasks/references/task-research.md b/av-dev-tasks/skills/tasks/references/task-research.md index e235b91..a097a82 100644 --- a/av-dev-tasks/skills/tasks/references/task-research.md +++ b/av-dev-tasks/skills/tasks/references/task-research.md @@ -75,9 +75,9 @@ ## Что видит машина, а что человек -`check` и `ready` смотрят на **наличие непустых** разделов `Вопрос` и -`Куда ляжет ответ`, считают сырьё отдельной строкой здоровья и держат его в конце -секции. Годность вопроса — человеку: «вопрос это или тема» машина не различает, +`ready` смотрит на **наличие непустых** разделов `Вопрос` и +`Куда ляжет ответ`; `check` считает сырьё отдельной строкой здоровья и держит +его в конце секции. Годность вопроса — человеку: «вопрос это или тема» машина не различает, и `check` о годности молчит намеренно. Штурм сырья, дробление исхода на задачи и тест «части мерджатся порознь» — diff --git a/av-dev-tasks/skills/tasks/scripts/tasks.py b/av-dev-tasks/skills/tasks/scripts/tasks.py index 24d3250..28300fe 100755 --- a/av-dev-tasks/skills/tasks/scripts/tasks.py +++ b/av-dev-tasks/skills/tasks/scripts/tasks.py @@ -3,8 +3,9 @@ Преемник backlog.py. Разница по существу одна: **секция-как-уровень заменена целью** (`goal:<слаг>` тегом), а приоритет стал тем, чем он и является, — -**порядком строк в беклоге**. Индексов три, и задача живёт ровно в одном из них -за раз. +**порядком строк в беклоге**. Индексов два — беклог и роадмап, — и задача живёт +ровно в одном из них за раз. `REJECTED.md` индексом не считается: он не говорит, +где запись числится, он кладбище ушедшего. Раскладка. Путь каталога — `tasks/` в корне репозитория, жёстко. Каталог принадлежит этому плагину, а не канону документов: `docs/` ведёт другой плагин, и @@ -66,7 +67,7 @@ goal | feature | fix | chore | research, по-английски, как и пр [--section S] [--goal G] [--why H] [--reason R] [--tag a,b] [--dir DIR] tasks.py edit S [--title T] [--why H] [--type T] [--goal G] [--add-tag a,b] [--rm-tag c,d] [--section S] [--dir DIR] - tasks.py move S --section S [--reason R] [--after S | --first] [--dir DIR] + tasks.py move S [--section S] [--reason R] [--after S | --first] [--dir DIR] tasks.py close S (--reason R | --implemented) [--dir DIR] tasks.py reopen S [--reason R] [--dir DIR] tasks.py ready S [S …] [--dir DIR] @@ -659,9 +660,10 @@ def raw_last(lines: list[str], raw: set[str]) -> list[str]: """Строки индекса, у которых сырьё снесено в конец своей секции. Сырьё (`research` без раздела «Вопрос») в работу не берётся, и стоя между - берущимися оно каждый раз требует открыть файл, чтобы это понять. Порядка - «по важности» в беклоге по-прежнему нет: этот порядок **производен от - типа**, а не назначен человеком, — потому его и можно проверять машиной. + берущимися оно каждый раз требует открыть файл, чтобы это понять. Порядок + строк в беклоге — приоритет, и назначает его человек; место сырья — + единственное исключение, и оно **производно от типа**, а не назначено, — + потому его и можно проверять машиной. Переставляются только сами строки-пункты, по своим же позициям: проза внутри секции, отбивка и заголовки остаются на месте. @@ -1265,6 +1267,26 @@ def health(lay: Layout, tasks: dict, entries: dict, sections: dict) -> None: else " — прочим не хватает разделов своего типа, цели или ждут" " ответа на вопрос")) + # Схема типа — **своя** строка, а не дубль предыдущей. «Готово к взятию» + # считает только беклог и валит запись за что угодно (вопрос, отсутствие + # цели, разделы); эта называет ровно одну причину и судит **все** записи, + # включая цели, которых `ready` не смотрит вовсе. Строка нужна потому, что + # обязательность разделов проверяет только `ready` на входе в работу, а + # между заведением и взятием запись иначе не судит никто. + bad_schema = {n: t for n, t in tasks.items() + if t["type"] in TYPE_SCHEMA and schema_verdict(lay, t)[0]} + if bad_schema: + unfit = sorted(n[:-3] for n in bad_schema) + # Сырьё названо отдельно: схему оно не выполняет по определению («Вопрос» + # пуст — тем оно и сырьё), и без этой оговорки счётчик читался бы как + # число недоделанных задач, хотя часть его — записи, ещё не ставшие ими. + crude = sum(1 for t in bad_schema.values() if raw_research(lay, t)) + print(f" схема типа не выполнена: {len(unfit)} из {len(tasks)}" + f" ({', '.join(unfit[:5])}{', …' if len(unfit) > 5 else ''})" + + (f", сырья из них {crude}" if crude else "") + + " — нет разделов, которых требует тип; чего именно, скажет" + " `tasks.py ready <слаг>`") + questions = [n for n, t in tasks.items() if questions_open(lay, t)] if questions: print(f" с открытым вопросом: {len(questions)}" @@ -1483,6 +1505,15 @@ def find_section(lines: list[str], name: str) -> tuple[int | None, str]: return None, "" +def section_at(lines: list[str], i: int) -> str: + """Секция, в которой лежит строка `i`, — ближайший заголовок выше неё. + + Дом у этого вопроса один: его задаёт и перестановка внутри секции + (`move` без `--section`), и починка «строка не в своей секции».""" + return next((m.group(1) for j in range(i, -1, -1) + if (m := SECTION.match(lines[j]))), "") + + def find_entry_index(lines: list[str], slug: str) -> int | None: for i, line in enumerate(lines): m = INDEX_ENTRY.match(line) @@ -1507,8 +1538,9 @@ def locate_all(lay: Layout, slug: str) -> dict[str, tuple[list[str], int]]: def insert_entry(lines: list[str], section: str, entry: str, after: str | None = None, first: bool = False) -> None: """Вставляет строку в секцию: по умолчанию в конец, --after <слаг> — следом - за указанной строкой, --first — первой. Порядок нужен только упорядоченной - части роадмапа; в беклоге он значения не имеет.""" + за указанной строкой, --first — первой. Позиция значима прежде всего в + беклоге: порядок строк там и есть приоритет (правило 4), и назначает его + человек — отсюда и умолчание «в конец», а не «наверх».""" hi, _ = find_section(lines, section) if hi is None: raise Usage(f"секции «{section}» в индексе нет") @@ -1774,13 +1806,18 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int: tags = [t for t in tags if not t.startswith(LEGACY_KIND_TAG)] why = task["why"] if a.why is None else a.why - section = task["section"] + # Написание секции берётся как есть, а не в нижнем регистре: имя секции + # принадлежит **заголовку индекса**, и мета на него только ссылается (тот же + # довод, что у шага 6 `apply_fixes`). Нижний регистр уезжал бы в файл + # «Категория: ядро», а следующий `check --fix` чинил бы за собственной + # правкой. Сверка принадлежности всё равно идёт по нижнему регистру. + section = task["section_raw"] if a.section is not None: if old_home == new_home: raise Usage("--section у edit — только вместе со сменой типа, меняющей" f" индекс. Секция внутри индекса — это move (он пишет причину):" f" tasks.py move {a.slug} --section {a.section} --reason …") - section = a.section.strip().lower() + section = a.section.strip() # Смена типа между целью и задачей — это переезд между индексами, а не # отказ: задача лежит ровно в одном индексе, неоднозначности нет. @@ -1792,7 +1829,7 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int: raise Usage(f"смена типа переносит строку в {lay.name(new_home)}," f" а секции «{section}» там нет (есть: {avail}) —" f" задай `--section <из перечисленных>`") - section = section_name.lower() + section = section_name # Тип передаётся всегда, а не только при `--type`: у файла, не переехавшего # на поле, он выведен из прежнего дома, и без него пересборка меты назвала @@ -1851,6 +1888,14 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int: def cmd_move(lay: Layout, a: argparse.Namespace) -> int: + """Перестановка строки: внутри своей секции или в другую. + + `--section` необязателен, и это не удобство. Перестановка внутри секции — + самая частая операция груминга (`move --after` и есть расстановка + приоритета), а требовать в ней повторить текущую секцию значит приглашать + указать не ту: перенос в чужую секцию выглядел бы ровно так же. Без + `--section` секция берётся из индекса — та, в которой строка уже лежит. + """ for err in (bad_slug(a.slug), bad_reason(a.reason), bad_slug(a.after) if a.after else None): if err: raise Usage(err) @@ -1865,10 +1910,17 @@ def cmd_move(lay: Layout, a: argparse.Namespace) -> int: f" ({', '.join(lay.name(k) for k in places)}) — неоднозначно," f" разбери сам: tasks.py check") kind_index, (lines, ei) = next(iter(places.items())) - hi, section = find_section(lines, a.section) - if hi is None: - avail = ", ".join(n for _, n in section_headers(lines)) - raise Usage(f"нет секции «{a.section}» в {lay.name(kind_index)} (есть: {avail})") + if a.section is None: + section = section_at(lines, ei) + if not section: + raise Usage(f"строка {a.slug} в {lay.name(kind_index)} стоит до первой" + f" секции — переставлять внутри нечего. Назови секцию:" + f" tasks.py move {a.slug} --section <секция> --reason …") + else: + hi, section = find_section(lines, a.section) + if hi is None: + avail = ", ".join(n for _, n in section_headers(lines)) + raise Usage(f"нет секции «{a.section}» в {lay.name(kind_index)} (есть: {avail})") task = parse_task(path) new_text = meta_updated(path, section=section, reason=a.reason, rtype=task["type"] or None) @@ -1888,7 +1940,10 @@ def cmd_move(lay: Layout, a: argparse.Namespace) -> int: plan.file(path, new_text) plan.index(lay, kind_index, lines) plan.commit() - print(f"{a.slug}: перенесено в «{section}» ({lay.name(kind_index)})") + where = ("первой" if a.first else f"после {a.after}" if a.after else "в конец") + print(f"{a.slug}: " + (f"перенесено в «{section}»" if a.section is not None + else f"переставлено внутри «{section}»") + + f", {where} ({lay.name(kind_index)})") return EXIT_OK @@ -1946,7 +2001,7 @@ def cmd_close(lay: Layout, a: argparse.Namespace) -> int: reason = a.reason.rstrip() dot = "" if reason.endswith((".", "!", "?")) else "." bullet = (f"- {today} `{a.slug}` — {task['title']}. Причина: {reason}{dot}" - f" Была секция: {task['section'] or '—'}.") + f" Была секция: {task['section_raw'] or '—'}.") rej = lay.index("rejected") prev = rej.read_text(encoding="utf-8") if rej.exists() else "# Ушедшее без реализации\n" if not prev.endswith("\n"): @@ -2039,7 +2094,9 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int: plan.file(path, text) target = home_index({"type": rtype}) - section = tmp["section"] + # Написание — из меты как есть: имя секции уедет в доклад, а сверка + # принадлежности всё равно идёт по нижнему регистру (`find_section`). + section = tmp["section_raw"] or tmp["section"] lines = read_lines(lay.index(target)) # Строка достигнутого снимается ДО вставки и на том же списке: иначе вторая # правка читает индекс с диска, где первой ещё нет, и затирает её. @@ -2058,6 +2115,16 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int: avail = ", ".join(n for _, n in section_headers(lines)) raise Usage(f"секции «{section}» нет в {lay.name(target)} (есть: {avail})") insert_entry(lines, sec, entry_line(lay, title, a.slug, tmp["why"])) + if target == "backlog": + # Место сырья производно от типа, и `reopen` обязан его соблюсти + # сразу: вернуть разведку без «Вопроса» просто в конец секции — + # значит поставить её после сырья, лежавшего там раньше, и получить + # ошибку `check` на ровном месте. Возвращаемого файла ещё нет на + # диске, поэтому он добавляется к набору вручную. + raw = {n for n, t in tasks_of(lay).items() if raw_research(lay, t)} + if raw_research(lay, tmp): + raw.add(f"{a.slug}.md") + lines[:] = raw_last(lines, raw) plan.index(lay, target, lines) elif unachieved: plan.index(lay, target, lines) @@ -2378,8 +2445,7 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]: if ei is None: raise RuntimeError(f"{name}: строка в {lay.name(kind)} пропала посреди" f" прохода — чинить нечего, отчёт был бы враньём") - cur = next((m.group(1) for j in range(ei, -1, -1) - if (m := SECTION.match(idx[kind][j]))), None) + cur = section_at(idx[kind], ei) if cur and cur.lower() != section.lower(): insert_entry(idx[kind], section, idx[kind].pop(ei)) fixed.append(f"{lay.name(kind)}: перенесена в секцию «{section}»: {name}") @@ -3012,11 +3078,11 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int: print(f" задач, не собравших разделы своего типа: {len(unfit)} —" f" check это ошибкой не считает, но `ready` их не пропустит:" f" брать сегодня физически нечего") - print(f" закрывается порциями груминга по 5–8 задач (скилл groom):" - f" проставить цели, превратить «готово, когда» в критерии с оракулами," - f" вынуть вопросы из прозы в раздел. Готовность к первой задаче —" - f" не «check зелёный», а «есть {CRITERIA_MIN}+ критериев хотя бы у набора" - f" под одну цель».") + print(" закрывается порциями груминга по 5–8 задач (скилл groom):" + " проставить цели, превратить «готово, когда» в критерии с оракулами," + " вынуть вопросы из прозы в раздел. Готовность к первой задаче —" + " не «check зелёный», а «`ready` пропускает хотя бы верхние строки" + " очереди»: берут по одной, и годной обязана быть та, которую берут.") print(" источники не удалены: сверь глазами и убери сам" f" ({', '.join(pl.get('sources', []))}) — удалять чужое молча нельзя.") print(" подписи ссылок машина не трогает: цель ссылки поправлена, а текст" @@ -3069,12 +3135,14 @@ def main() -> int: p.add_argument("--section", help="только вместе со сменой типа, меняющей индекс") p.add_argument("--dir") - p = sub.add_parser("move", help="перенести в другую категорию беклога или часть роадмапа") + p = sub.add_parser("move", help="переставить строку: место в очереди или другая секция") p.add_argument("slug") - p.add_argument("--section", required=True) + p.add_argument("--section", help="другая категория беклога или часть роадмапа;" + " без него — текущая секция записи") p.add_argument("--reason") g = p.add_mutually_exclusive_group() - g.add_argument("--after", help="встать следом за этим слагом (упорядоченная часть роадмапа)") + g.add_argument("--after", help="встать следом за этим слагом — расстановка" + " приоритета: порядок строк беклога это очередь") g.add_argument("--first", action="store_true") p.add_argument("--dir")