задачи: починена смена стадии, разобраны находки ревью плагина

Команда stage была дефектна по шести пунктам, и все шесть подтверждены
прогоном: не звала raw_last (переход оставлял каталог красным), не
переписывала шапку беклога (индекс продолжал объявлять прежнюю стадию),
шла в обход write_config, молча пропускала файлы с непересобираемой метой,
ломалась на беклоге без заголовков и схлопывала полки при первом
объявлении стадии.

Объявление и смена разведены: объявление беклога не трогает вовсе, смена
трогает состав секций только по явному --sections, а слить полки скрипт
не берётся ни в одном случае. Абзац шапки размечен парой «стадия», и
расхождение с конфигом стало обычным дрейфом.

Отказ по недостающей строке индекса запирал запись, пережившую упразднение
роадмапа: edit, close и reopen теперь заводят или пропускают строку сами.
Прочее: регистр stage нормализуется при чтении; --fix снимает мёртвые теги
и у неразобранных записей; move отказывает переставлять сырьё; adopt
держит место сырья; docs.py bump двигает одну запись журнала за раз;
tasks.py получил перечень упразднённых адресов, и гейт наконец видит
собственное упразднение ROADMAP.md.

Запись «Версия 3» переписана по прогону на игрушечном проекте: прежний
порядок шагов был неисполним. Закрыты дыры модели стадий (пересмотр плана
стройки стал сценарием, приёмка отвязана от груминга, from-review,
research и adopt получили развилку по стадии, перечень осей пересчитан) и
находки, старшие этой сессии: review-triage получил режим без метки, три
списка проектных копий сведены к дому с проверяемыми копиями, пять
пересказов правил стали помеченными копиями или ссылками, language.md
перестал объявлять юрисдикцию над чужим плагином.
This commit is contained in:
av
2026-08-13 15:08:29 +03:00
parent 8d8c1656e5
commit ed83ec7dc0
28 changed files with 678 additions and 259 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
{
"name": "av-dev",
"source": "./av-dev",
"description": "Личный процесс разработки одним плагином: форма раскладки, документы проекта, учёт работ и работа по задачам. Форму держит скилл canon — три операции одной машиной сравнения (check, adopt, upgrade) со скриптом docs.py и общим журналом версий, по которому повышается вся раскладка, включая каталог задач; имя без префикса, потому что форма общая у всех частей. Документы — канон раскладки (CLAUDE.md плюс docs/: паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), заведение нового проекта интервью в doc-init, ведение содержимого по ходу разработки в doc-sync, суд о смысловом здоровье в doc-healthcheck двумя агентами разом. Учёт — задачи и цели каталогом markdown-файлов в task-track, у записи тип (goal, feature, fix, chore, research), и тип решает её схему; приоритет расставляет интерактивный task-groom порядком строк. Работа — одна задача от постановки до закрытия скиллом code-resolve: точка входа одна, сценария три (решение циклом SDD с чекпоинтом, разведка без кода, обслуживание без change), и выбирает сценарий сам скилл, прочитав постановку; плюс конвейер ревью code-review с детерминированным гейтом, сверкой со спеками и обязательным триажем, плюс code-openspec, который заводит и проверяет openspec/. Версия раскладки и настройки живут в .av-dev.toml в корне репозитория. Части включаются следом в проекте, а не установкой: нет docs/ — документы не ведутся, нет каталога задач — учёт остаётся владельцу, нет openspec/ — его заводит сценарий решения. Коммиты — отдельный плагин av-dev-git."
"description": "Личный процесс разработки одним плагином: форма раскладки, документы проекта, учёт работ и работа по задачам. Форму держит скилл canon — три операции одной машиной сравнения (check, adopt, upgrade) со скриптом docs.py и общим журналом версий, по которому повышается вся раскладка, включая каталог задач; имя без префикса, потому что форма общая у всех частей. Документы — канон раскладки (CLAUDE.md плюс docs/: паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), заведение нового проекта интервью в doc-init, ведение содержимого по ходу разработки в doc-sync, суд о смысловом здоровье в doc-healthcheck двумя агентами разом. Учёт — задачи каталогом markdown-файлов в task-track: у задачи тип (feature, fix, chore, research), задающий её схему, а у проекта — стадия (build — беклог это план стройки от базы к деталям, порядок строк значит зависимость; support — очередь правок, порядок значит важность); очередь расставляет интерактивный task-groom порядком строк. Работа — одна задача от постановки до закрытия скиллом code-resolve: точка входа одна, сценария три (решение циклом SDD с чекпоинтом, разведка без кода, обслуживание без change), и выбирает сценарий сам скилл, прочитав постановку; плюс конвейер ревью code-review с детерминированным гейтом, сверкой со спеками и обязательным триажем, плюс code-openspec, который заводит и проверяет openspec/. Версия раскладки и настройки живут в .av-dev.toml в корне репозитория. Части включаются следом в проекте, а не установкой: нет docs/ — документы не ведутся, нет каталога задач — учёт остаётся владельцу, нет openspec/ — его заводит сценарий решения. Коммиты — отдельный плагин av-dev-git."
},
{
"name": "av-dev-git",
+17 -6
View File
@@ -241,11 +241,22 @@ claude plugin install av-dev-git@av-dev-skills --scope project
}
```
**При установке в проект, где лежали проектные копии** скиллов и агентов
(`.claude/skills/` — голые `task-pipeline`, `review-pipeline`
и с префиксом проекта `<проект>-task-pipeline`,
`.claude/agents/<проект>-review-*.md`) — снеси их. Две копии одного скилла
расходятся, и побеждает та, что короче названа.
**При установке в проект, где лежали проектные копии** скиллов и агентов
снеси их. Перечень полный, и он же дом: скиллы носят его помеченной копией,
потому что предупреждают о том же в момент работы.
<!-- дом: проектные-копии -->
Голые имена в `.claude/skills/`: `resolve`, `review`, а у проектов прошлого
поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`. С префиксом
проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`. Агенты:
`.claude/agents/<проект>-review-*.md`.
Две копии одного скилла расходятся, и побеждает та, что **короче названа**:
короткое имя разрешится в устаревшую проектную копию молча и без признаков
подмены.
<!-- /дом: проектные-копии -->
## Обновление
@@ -569,7 +580,7 @@ Glob разводит две половины: коммит, трогающий
**Что судит `decisions.py`.** Номер записи журнала обязан быть один: буквенные
метки решений выродились до пятибуквенных и разъехались молча — `АЕАКЛ` означала
и тему 53, и тему 65. И подпись ссылки обязана называть свою цель: в
`[тема 5](05-project-start-lifecycle.md)` номер записан дважды, словом и путём, —
`[тема 5](decisions/05-project-start-lifecycle.md)` номер записан дважды, словом и путём, —
дубль неизбежный (иначе ссылку не прочитать, не открыв) и потому сверяемый, как
всякая копия.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "av-dev",
"description": "Личный процесс разработки одним плагином: форма раскладки, документы проекта, учёт работ и работа по задачам. Форму держит скилл canon — три операции одной машиной сравнения (check, adopt, upgrade) со скриптом docs.py и общим журналом версий, по которому повышается вся раскладка, включая каталог задач; имя без префикса, потому что форма общая у всех частей. Документы — канон раскладки (CLAUDE.md плюс docs/: паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), заведение нового проекта интервью в doc-init, ведение содержимого по ходу разработки в doc-sync, суд о смысловом здоровье в doc-healthcheck двумя агентами разом. Учёт — задачи и цели каталогом markdown-файлов в task-track, у записи тип (goal, feature, fix, chore, research), и тип решает её схему; приоритет расставляет интерактивный task-groom порядком строк. Работа — одна задача от постановки до закрытия скиллом code-resolve: точка входа одна, сценария три (решение циклом SDD с чекпоинтом, разведка без кода, обслуживание без change), и выбирает сценарий сам скилл, прочитав постановку; плюс конвейер ревью code-review с детерминированным гейтом, сверкой со спеками и обязательным триажем, плюс code-openspec, который заводит и проверяет openspec/. Версия раскладки и настройки живут в .av-dev.toml в корне репозитория. Части включаются следом в проекте, а не установкой: нет docs/ — документы не ведутся, нет каталога задач — учёт остаётся владельцу, нет openspec/ — его заводит сценарий решения. Коммиты — отдельный плагин av-dev-git.",
"description": "Личный процесс разработки одним плагином: форма раскладки, документы проекта, учёт работ и работа по задачам. Форму держит скилл canon — три операции одной машиной сравнения (check, adopt, upgrade) со скриптом docs.py и общим журналом версий, по которому повышается вся раскладка, включая каталог задач; имя без префикса, потому что форма общая у всех частей. Документы — канон раскладки (CLAUDE.md плюс docs/: паспорт, архитектура, схема БД, безопасность, конвенции, разведка, ADR, журнал ревью), заведение нового проекта интервью в doc-init, ведение содержимого по ходу разработки в doc-sync, суд о смысловом здоровье в doc-healthcheck двумя агентами разом. Учёт — задачи каталогом markdown-файлов в task-track: у задачи тип (feature, fix, chore, research), задающий её схему, а у проекта — стадия (build — беклог это план стройки от базы к деталям, порядок строк значит зависимость; support — очередь правок, порядок значит важность); очередь расставляет интерактивный task-groom порядком строк. Работа — одна задача от постановки до закрытия скиллом code-resolve: точка входа одна, сценария три (решение циклом SDD с чекпоинтом, разведка без кода, обслуживание без change), и выбирает сценарий сам скилл, прочитав постановку; плюс конвейер ревью code-review с детерминированным гейтом, сверкой со спеками и обязательным триажем, плюс code-openspec, который заводит и проверяет openspec/. Версия раскладки и настройки живут в .av-dev.toml в корне репозитория. Части включаются следом в проекте, а не установкой: нет docs/ — документы не ведутся, нет каталога задач — учёт остаётся владельцу, нет openspec/ — его заводит сценарий решения. Коммиты — отдельный плагин av-dev-git.",
"author": {
"name": "Anton Vakhrushev",
"email": "anwinged@gmail.com"
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: doc-wording
description: "Вычитка языка документов проекта по информационному стилю — паспорт, архитектура, конвенции, безопасность, решения ADR, записки разведки, CLAUDE.md. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта, транслит в имени файла. Отдаёт готовые формулировки на замену и ничего не правит сам. Записи каталога задач вычитывает отдельный агент task-wording, их форму — task-form. Зовётся по названной пачке правленных документов, а не на весь канон: последним шагом синка документации (av-dev:doc-sync), шагом заведения проекта (av-dev:doc-init), шагами adopt и upgrade скилла av-dev:canon. Скилл healthcheck его не зовёт — там сверка утверждений, а не языка. Только чтение."
description: "Вычитка языка документов проекта по информационному стилю — паспорт, архитектура, конвенции, безопасность, решения ADR, записки разведки, CLAUDE.md. Смотрит отглагольные существительные и страдательный залог, оценку без факта, стоп-слова и канцелярит, «одна мысль — одно предложение», англицизм при живом русском слове, жаргон и метафоры вместо прямого называния, термин, которого нет в документах проекта, транслит в имени файла. Отдаёт готовые формулировки на замену и ничего не правит сам. Записи каталога задач вычитывает отдельный агент task-wording, их форму — task-form. Зовётся по названной пачке правленных документов, а не на весь канон: последним шагом синка документации (av-dev:doc-sync), шагом заведения проекта (av-dev:doc-init), сценарием разведки (av-dev:code-resolve), шагами adopt и upgrade скилла av-dev:canon. Скилл healthcheck его не зовёт — там сверка утверждений, а не языка. Только чтение."
tools: Read, Grep, Glob
model: sonnet
color: green
+12 -4
View File
@@ -213,11 +213,15 @@ color: green
- **незнакомое** — форму предстоит нащупать по ходу. Признак один и
проверяемый: **перед работой нельзя назвать, какие узлы будут тронуты**.
| | знакомое | незнакомое |
<!-- копия: матрица-метки из av-dev/skills/code-review/references/review-levels.md -->
| | **знакомое** — форму решения можно назвать до начала | **незнакомое** — форму предстоит нащупать по ходу |
|---|---|---|
| **малое** | `small` | `large` |
| **среднее** | `medium` | `large` |
| **крупное** | `large` | `large` |
| **малое** — один узел | `small` | `large` |
| **среднее** — несколько узлов одного слоя | `medium` | `large` |
| **крупное** — несколько слоёв, перенос ответственности, большой рефакторинг | `large` | `large` |
<!-- /копия: матрица-метки -->
**Метка — не синоним размера, и это главная ловушка таблицы.** Размер `малое` и
метка `small` совпадают только в левом верхнем углу: малое **незнакомое**
@@ -272,6 +276,8 @@ color: green
**Ревью кода — раздача тем.** Кто закрывает тему, зависит от метки. Раскладка
жёсткая, выдумывать её не надо:
<!-- копия: тема-метка-глубина из av-dev/skills/code-review/SKILL.md -->
| Тема | `small` | `medium` | `large` |
|---|---|---|---|
| `requirements` | `specs`, сверка | `specs`, разбор | `specs`, разбор |
@@ -282,6 +288,8 @@ color: green
| `operations` | `code`, сверка по инвариантам | `basics`, разбор | `ops`, доказательство |
| тема проекта | `basics`, сверка | `basics`, разбор | `basics`, разбор |
<!-- /копия: тема-метка-глубина -->
Две глубины, которые ты назначаешь:
- **сверка** — открыть дом, открыть дифф, сравнить. Один-два вопроса на тему,
+28 -15
View File
@@ -1,6 +1,6 @@
---
name: review-triage
description: "Обязательный финальный проход конвейера ревью — единственный, кто агрегирует. Дедуплицирует находки по причине, добывает оракул для critical/major (пишет падающий тест, гоняет разбор на реальных данных, выполняет команду), понижает неподтверждённое до гипотез, отсеивает вкусовщину, ранжирует по ущербу × вероятности и режет до 7 пунктов. Помечает каждую находку «инлайн» или «развилка» для оркестратора. Сверяет план разметки задачи с пришедшими отчётами: тема, размеченная и оставшаяся без отчёта, — находка о самом прогоне. Формирует итоговый отчёт с планом, перечнем проходов и обязательной секцией границ покрытия."
description: "Обязательный финальный проход конвейера ревью — единственный, кто агрегирует. Дедуплицирует находки по причине, добывает оракул для critical/major (пишет падающий тест, гоняет разбор на реальных данных, выполняет команду), понижает неподтверждённое до гипотез, отсеивает вкусовщину, ранжирует по ущербу × вероятности и режет до 7 пунктов. Помечает каждую находку «инлайн» или «развилка» для оркестратора. Сверяет план с пришедшими отчётами: тема, стоявшая в плане и оставшаяся без отчёта, — находка о самом прогоне; на прогоне без метки план даёт сценарий обслуживания, а не разметчик. Формирует итоговый отчёт с планом, перечнем проходов и обязательной секцией границ покрытия."
tools: Read, Grep, Glob, Bash, Write
model: opus
color: yellow
@@ -21,18 +21,26 @@ color: yellow
## Вход
Сырые выводы всех запущенных проходов, `git diff <база>..HEAD`, **план разметки
задачи** (агент `review-scope`, один запуск после `propose`) и режим прогона.
Дельта-спеки — по мере надобности.
Сырые выводы всех запущенных проходов, `git diff <база>..HEAD`, **план прогона**
и режим. Дельта-спеки — по мере надобности.
План — это таблица «тема → дом → глубина → кто закрывает» плюс размер, сложность
и метка с обоснованием. Он твой главный инструмент сверки: ты единственный, кто
видит и то, что размечено, и то, что пришло.
План — таблица «тема → дом → глубина → кто закрывает». Он твой главный инструмент
сверки: ты единственный, кто видит и то, что заявлено, и то, что пришло.
**Плана нет — ты не запускаешься, и исключений нет.** Сверка размеченного с
пришедшим — твоя единственная защита от молчащего пропуска, и без плана она не
выполняется вовсе. Отчёт, собранный без неё, выглядит полным ровно настолько же,
насколько и неполный.
**Откуда план приходит, зависит от режима, и режимов два.**
- **С меткой** — план собрал `review-scope` (один запуск после `propose`), и к
таблице прилагаются размер, сложность и метка с обоснованием.
- **Без метки** — так идёт прогон сценария обслуживания: изменение не меняет
поведения, размечать нечего, и разметчик не запускается вовсе. План
**фиксирован сценарием** (`av-dev:code-resolve`, `references/maintain.md`), а
размера, сложности и метки не существует. Не ищи их и не подставляй: в отчёте
на их месте — строка «прогон без метки, план сценария».
**Плана нет ни от разметчика, ни от сценария — ты не запускаешься, и исключений
нет.** Сверка заявленного с пришедшим — твоя единственная защита от молчащего
пропуска, и без плана она не выполняется вовсе. Отчёт, собранный без неё,
выглядит полным ровно настолько же, насколько и неполный.
Из документов проекта тебе нужны:
@@ -174,7 +182,9 @@ severity:
**Сигнала нет — тоже скажи строкой.** «Корректор метки отработал, возражений
нет» и «корректор не запускался» — разные вещи, и отличить их по молчанию
нельзя.
нельзя. **На прогоне без метки корректору нечего поднимать**, и это третье
состояние: пиши «метки нет, корректор неприменим», а не «не запускался» —
последнее читается как пропуск.
## Границы покрытия — не сокращаются
@@ -238,9 +248,12 @@ severity:
Строго секциями из контракта: `Блокирует мердж` (≤3) / `Стоит исправить сейчас`
(≤4) / `Гипотезы без доказательства` / `Promote candidates` / `Границы покрытия`.
Перед секциями — сводка: размер, сложность и метка с обоснованием разметки и режим прогона,
состояние гейта, **план с исходом по каждой теме**, сколько находок пришло на
вход и сколько осталось.
Перед секциями — сводка: режим прогона, состояние гейта, **план с исходом по
каждой теме**, сколько находок пришло на вход и сколько осталось. На прогоне
**с меткой** к этому добавляются размер, сложность и метка с обоснованием
разметки; на прогоне **без метки** их место занимает строка «прогон без метки,
план сценария обслуживания» — выдумывать метку задним числом нельзя, её никто
не снимал.
## Ограничения
-1
View File
@@ -98,7 +98,6 @@ color: green
постановке. Он же путь понизить требования решением, принятым до
проектирования.
## Чего ты не проверяешь
Не своё бывает двух разных родов, и поступают с ними по-разному.
+14 -2
View File
@@ -38,7 +38,10 @@
| --- | --- | --- |
| стадия проекта | что значит порядок строк беклога: зависимость или важность | `task-track/SKILL.md`, «Две стадии» |
| стадия проекта | сколько у беклога секций, как его пополняют, применим ли груминг | там же и `task-groom/SKILL.md`, «Груминг — операция доработки» |
| стадия проекта | метку, глубину и тип**не влияет, и это записано явно** | `task-track/SKILL.md`, «Тип записи» |
| стадия проекта | метку и глубину — **не влияет, и это записано явно** | `task-track/SKILL.md`, «Тип записи» |
| стадия проекта | тип записи — **не влияет, и это записано явно** | `task-track/SKILL.md`, «Две стадии» |
| стадия проекта | как серьёзность находки ложится в список | `task-track/references/from-review.md` |
| стадия проекта | где сценарии кладут свой исход и чем закрывают переходное состояние | `code-resolve/references/research.md`, `task-track/references/adopt.md` |
| тип записи | сценарий — **предлагает**, подтверждает предмет работы | `task-track/SKILL.md`, «Тип записи» |
| тип записи | метку и глубину — **не влияет, и это записано явно** | там же |
| сценарий | режим прогона: обслуживание идёт без метки | `code-resolve/references/maintain.md` |
@@ -48,7 +51,7 @@
| категория документа | заводит ли документ направление проверки | `canon.md`, «Три категории» |
| severity | что с находкой делают дальше | `code-review/SKILL.md`, «Что происходит с находками» |
**Три клетки пусты, и это сказано намеренно, а не забыто.**
**Пять клеток пусты, и это сказано намеренно, а не забыто.**
**Категория документа × режим прогона.** На прогоне **с меткой** своя тема
проекта закрыта при любом значении: `review-basics` — приёмник проектных тем и
@@ -62,6 +65,15 @@
неё не входит. Заманчивая мысль «на стройке всё `small`, потому что приложения
ещё нет» разбивается о первый же шаг, кладущий схему хранилища.
**Стадия проекта × режим прогона и × стадия ревью.** Не влияет ни на одну:
режим выбирает сценарий, стадию ревью — наличие дизайна. Прогон обслуживания на
стройке — обычное дело (первые шаги плана заводят гейт и сборку), и идёт он там
так же, как на доработке.
**Стадия проекта × категория документа и × коды выхода.** Не влияет: категория —
свойство документа, коды — общий словарь скриптов. Названо потому, что перечень
объявлен полным, и клетка без ответа читается как забытая.
**Режим прогона × severity.** Триаж обязателен всегда, в том числе без метки. Но
часть оснований `critical` — построенный путь к отказу, замер — добывается
проходами, которые без метки не запускаются. Значит ли это, что `critical` на
+14 -5
View File
@@ -21,9 +21,18 @@
проверяет вовсе, — а вложенных блоков разметка не знает, и внутри `язык-правила`
его было бы не забрать отдельно.
Правила — для всего, что пишется словами: задачи и цели, документы канона,
решения ADR, записки разведки, сообщения коммитов. Не для кода и не для
сообщений программы пользователю — там свои конвенции проекта.
Правила — для всего, что пишется словами **в этом плагине**: задачи, документы
канона, решения ADR, записки разведки. Не для кода и не для сообщений программы
пользователю — там свои конвенции проекта.
**Сообщения коммитов сюда не входят, и это названо намеренно.** Их форму держит
отдельный плагин `av-dev-git`, скилл `commit`, и она с этими правилами
расходится по существу: там предписан результат страдательным залогом
(«добавлены», «обновлено»), здесь — действие активным. Расхождение осознанное:
строка коммита отвечает на «что стало», а не на «что я сделал», и читают её в
`git log` подряд сотнями. Объявлять юрисдикцию над чужим плагином, до которого
отсюда нет и ссылки, значило бы завести правило, нарушение которого никто не
увидит.
Основа — **информационный стиль** Максима Ильяхова ([учебник
бюро](https://bureau.ru/projects/book-text/), книга «Пиши, сокращай»). Он
@@ -228,8 +237,8 @@
## Доклад вычитки
Не правило языка, а **контракт прохода**: форма, в которой находка приходит к
человеку. Живёт здесь потому, что проходов вычитки несколько — по одному на
плагин, — и разойтись формой они не должны.
человеку. Живёт здесь потому, что проходов вычитки два — `doc-wording` по документам и
`task-wording` по записям задач, — и разойтись формой они не должны.
<!-- дом: вычитка-доклад -->
+5 -4
View File
@@ -255,10 +255,11 @@ capability), `openspec/config.yaml`.
**Задачи `docs.py` не проверяет** — их ведёт другой скилл, и согласованность
каталога показывает только `tasks.py check`. Позвал на шаге 5 скилл задач —
его отчёт идёт в доклад отдельной строкой, и пункт «задачи без цели» в нём
зелёным не станет: цели не сочиняются адаптацией (запрет записан у того, кто
ведёт задачи), их проставляет человек порциями переоценки на первом груминге —
скилл `av-dev:task-groom`.
его отчёт идёт в доклад отдельной строкой, и зелёным он сразу не станет:
у перенесённых записей нет критериев приёмки, а `check` без объявленной
**стадии** отказывает вовсе. Стадию называет человек (`tasks.py stage
build|support`) — машина её не выводит: список пунктов одинаково выглядит и
планом стройки, и очередью правок.
### 5. Объяви переходное состояние
+4 -3
View File
@@ -351,9 +351,10 @@ kebab-case.** Причина не эстетическая: имя файла с
скриптом и своими проверками; где каталог лежит и как названы его части, говорит
секция `[tasks]` в `.av-dev.toml`. Версия там одна на всю раскладку: канон и
каталог задач двигаются вместе, потому что ведёт их один плагин.
Канон **резервирует место** в `docs/` и внутрь не смотрит:
`docs.py` каталог не открывает, его отсутствия не считает дрейфом и согласованность
задач не проверяет. Проект, не заведший каталог задач, их не ведёт
Каталог лежит **в корне репозитория, вне `docs/`** — места в `docs/` канон ему
больше не резервирует (переезд — запись 11 закрытого журнала), а внутрь не
смотрит и подавно: `docs.py` каталог не открывает, его отсутствия не считает
дрейфом и согласованность задач не проверяет. Проект, не заведший каталог задач, их не ведёт
вовсе, и отказом это быть не может.
Раскладку, форму записи и команды держит скилл `av-dev:task-track`. Ниже — то,
+50 -17
View File
@@ -40,27 +40,60 @@
`[tasks] completion_heading` — тоже. Появились ключ `[tasks] stage`, команда
`tasks.py stage` и флаги `init --stage`, `adopt scan --stage`.
**Что сделать проекту.**
**Что сделать проекту. Порядок шагов обязателен**, и первый шаг — не команда:
пока в `[tasks]` лежит упразднённый ключ, **любая** подкоманда `tasks.py`
отвечает кодом 3 и работать нечем.
1. **Разобрать цели.** У каждой записи типа `goal` в `tasks/items/` два исхода, и
выбирает человек: она становится обычной задачей (`edit <слаг> --type
feature|fix|chore|research`) либо уходит (`close <слаг> --reason …`). Задачи,
носившие её тег, живут дальше сами по себе. Скрипт этого не решает и говорит
1. **Вычистить конфиг руками.** Из секции `[tasks]` в `.av-dev.toml` удалить
ключи `roadmap` (или `plan`) и `completion_heading`. Каждый из них — код 3 на
любой команде, и названы они здесь оба: второй легко пропустить, потому что
его упразднение не видно по имени файла.
2. **Удалить `tasks/ROADMAP.md`.** Секция `Готово` уходит вместе с ним и **не
переносится**: «что приложение умеет» отвечают спеки, «когда это появилось» —
`git log` беклога. Проект без `openspec/specs/` теряет здесь единственный
связный перечень достигнутого — если он нужен, сохрани его сам до удаления
(документом проекта, не задачами).
3. **Прогнать `tasks.py check --fix`.** Он снимет теги `goal:<слаг>` и
`decomposed`, переименует поле `Секция``Категория` и перепишет старую
форму меты — **в том числе у самих записей типа `goal`**. Записи `goal` при
этом останутся: во что превращается цель, машина не решает и говорит
`НЕОДНОЗНАЧНО`.
2. **Перенести содержимое `ROADMAP.md`.** Секция `Готово` **удаляется**: «что
приложение умеет» отвечают спеки, «когда это появилось» — `git log`. Строки
`Запланировано`, `Направления` и `Сопровождение` — это цели, и они разбираются
шагом 1. Затем удалить сам файл и ключ `roadmap` из `.av-dev.toml`, если он там
был.
3. **Объявить стадию**`tasks.py stage build` или `tasks.py stage support`.
4. **Разобрать цели поштучно.** У каждой два исхода, и выбирает человек: она
становится задачей (`edit <слаг> --type feature|fix|chore|research`) либо
уходит (`close <слаг> --reason …`). Строки в беклоге у неё нет — её жильём
был роадмап, — и `edit --type` заведёт её сам, в первую секцию и в конец,
сказав об этом; место назначь потом. Раздел `Завершение` в теле переехавшей
записи **удали руками**: схеме нового типа он не принадлежит, и `check`
оставит о нём замечание. Задачи, носившие тег цели, живут дальше сами по
себе — разбирать их не нужно.
5. **Объявить стадию**`tasks.py stage build` или `tasks.py stage support`.
Приложение ещё строится и список работ линеен по зависимости — `build`;
работает и правится точечно — `support`. Без ключа `check` отказывает: порядок
строк нечем прочитать. На `build` секция беклога обязана остаться **одна**
слить полки надо руками, порядок строк в слитом списке знает только человек.
4. **Поднять версию**`docs.py bump`. Последним шагом.
5. `docs.py check` и `tasks.py check --dir <каталог задач>` — до отсутствия
дрейфа. Теги `goal:` и `decomposed`, поле `Секция` и старую форму меты снимет
`tasks.py check --fix`.
строк нечем прочитать.
**Объявление беклог не трогает** — ни секций, ни файлов: оно называет то, что
уже верно. Поэтому проекту с несколькими полками, объявляющему `build`,
команда откажет и назовёт выход: слить полки самому (`move <слаг> --section
<куда> --reason …`), потому что порядок строк в слитом списке знает только
человек. Флаг `--sections` при объявлении не принимается — он для **смены**
стадии, где сливать просят явно.
6. **Поправить шапку `BACKLOG.md`.** Абзац про стадию теперь размечен парой
`<!-- стадия -->``<!-- /стадия -->`, и по нему `check` сверяет шапку с
конфигом. В беклоге, заведённом до этой версии, разметки нет — `stage` об
этом скажет. Возьми готовый абзац из свежего каталога (`tasks.py init` во
временном месте) или напиши сам: он объясняет, что значит порядок строк, и
читают вместо документации именно его.
7. **Поднять версию**`docs.py bump`. Последним шагом. Он двигает **одну**
запись за раз: отставшему на две записи проекту зовётся дважды, следом за
шагами каждой.
8. `docs.py check` и `tasks.py check --dir <каталог задач>` — до отсутствия
дрейфа.
**Проект, не прошедший записи 1 и 2, начинает с этой.** Их собственные шаги
велят гонять `tasks.py check` до зелёного, а он на упразднённом ключе отвечает
кодом 3 — то есть пройти их сегодня нельзя, не сделав шаг 1 отсюда. Записи от
этого не переписываются: порядок между ними прежний, добавлено одно условие
входа.
---
+15 -2
View File
@@ -702,9 +702,22 @@ def cmd_bump(args: argparse.Namespace) -> int:
if was is not None and was > LAYOUT_VERSION:
fail(ENV, f"проект на версии {was}, скрипт знает {LAYOUT_VERSION}:"
f" устарел плагин, обнови маркетплейс")
conf.set_version(root, LAYOUT_VERSION)
# Двигается **одна** запись за раз, а не сразу до текущей: число объявляет
# пройденными шаги журнала, и прыжок через запись объявил бы пройденным то,
# чего никто не делал. Отставшему на три записи проекту `bump` зовётся три
# раза — по разу на запись, следом за её шагами.
#
# Версии нет вовсе — случай другой: проект не жил ни одной записью журнала,
# его раскладку только что вывели сегодняшним форматом (`adopt`), и
# объявлять ему нечего, кроме текущего числа.
target = LAYOUT_VERSION if was is None else was + 1
conf.set_version(root, target)
print(f"версия раскладки: {was if was is not None else 'не была объявлена'}"
f"{LAYOUT_VERSION} в {CONFIG}")
f"{target} в {CONFIG}")
if target < LAYOUT_VERSION:
print(f" до текущей ({LAYOUT_VERSION}) осталось записей журнала:"
f" {LAYOUT_VERSION - target}. Пройди шаги следующей и позови bump"
f" снова — по разу на запись")
return OK
+20 -12
View File
@@ -41,12 +41,20 @@ description: "Взять одну задачу и довести её до за
делает скилл `av-dev:code-openspec`. **Сценариям разведки и обслуживания
OpenSpec не нужен** — они не заводят change; `opsx:explore` берётся разведкой,
если плагин есть.
- **Проектные копии этих скиллов и агентов удаляются при установке плагина**
(`.claude/skills/` — голые имена `resolve`, `review`, а у проектов прошлого
поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`, и с префиксом
проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`;
`.claude/agents/<проект>-review-*.md`). Две копии одного скилла расходятся, и
побеждает та, что короче названа.
- **Проектные копии этих скиллов и агентов удаляются при установке плагина.**
<!-- копия: проектные-копии из README.md -->
Голые имена в `.claude/skills/`: `resolve`, `review`, а у проектов прошлого
поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`. С префиксом
проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`. Агенты:
`.claude/agents/<проект>-review-*.md`.
Две копии одного скилла расходятся, и побеждает та, что **короче названа**:
короткое имя разрешится в устаревшую проектную копию молча и без признаков
подмены.
<!-- /копия: проектные-копии -->
### Чего может не быть
@@ -108,8 +116,7 @@ description: "Взять одну задачу и довести её до за
**Запись из каталога сперва проверяется на готовность, и проверяет её машина.**
Вызови Skill `av-dev:task-track` и попроси прогнать `ready <слаг>`: он смотрит
тип, цель у `feature`, пустой ли раздел вопросов и собраны ли разделы схемы
типа. Судить это глазами нельзя — ровно тот случай, где машина дешевле и точнее,
тип, пустой ли раздел вопросов и собраны ли разделы схемы типа. Судить это глазами нельзя — ровно тот случай, где машина дешевле и точнее,
а цена ошибки отложенная: недостающие критерии приёмки обнаружатся на приёмке,
когда сверять уже не с чем.
@@ -301,13 +308,14 @@ flowchart TD
## Границы: чем этот скилл не владеет
- **Беклогом, целями и приоритетами.** Задача приходит извне. Скилл её не
выбирает, не приоритизирует, не заводит и не переоценивает.
- **Беклогом и порядком работ.** Задача приходит извне. Скилл её не выбирает,
не переставляет, не заводит и не переоценивает.
- **Форматом задач и документов.** Индексы и документы канона руками не правятся,
путь к чужому скрипту не выдумывается: этим владеют `av-dev:task-track` и
`av-dev:doc-sync`. Закрытие — работа этого скилла, и это осознанное решение с
названной ценой: **приёмщик и исполнитель совпали**. Закрытие поэтому **не окончательно** — человек на
груминге (`av-dev:task-groom`) возвращает задачу `reopen` с причиной, а доклад
названной ценой: **приёмщик и исполнитель совпали**. Закрытие поэтому **не окончательно** — человек
возвращает задачу `reopen` с причиной а доработке это делают грумингом,
`av-dev:task-groom`, на стройке — сразу, как заметили), а доклад
по критериям приёмки становится единственным, по чему приёмка вообще возможна.
- **Определением ценности.** «Нужна ли эта функциональность» — не вопрос этого
скилла ни на одном шаге и ни в одном сценарии. Чекпоинт решения спрашивает «так
@@ -81,10 +81,15 @@
- **чем задача становится** — `fix` или `feature`, с причиной из разреза выше;
- **что уже сделано** и что из этого лежит в рабочем дереве.
Проверка на простой язык та же, что у чекпоинтов двух других сценариев: **в
тексте нет `SHALL`, нет имён файлов и функций там, где вещь называется
Проверка на простой язык — общая у трёх сценариев:
<!-- копия: чекпоинт-простой-язык из av-dev/skills/code-resolve/references/solve.md -->
**в тексте нет `SHALL`, нет имён файлов и функций там, где вещь называется
по-русски, и нет слов, которых нет в паспорте проекта.**
<!-- /копия: чекпоинт-простой-язык -->
**3. Дай два решения и жди ответа.** Их ровно два, и оба законны:
- **переформулировать запись** — тип меняется на названный, и дальше задача идёт
@@ -172,9 +177,12 @@ flowchart TD
остаётся в рабочем дереве незакоммиченным. Доклад называет **оба**: какой тип
предложен и что человек выбрал;
- **нужна разведка** — форма правки неизвестна (мажорное обновление, смена
сборщика, переезд гейта на другой инструмент) **или сработал триггер ADR**:
дорогой откат, намеренный отказ от очевидного подхода, пересмотр прежнего
решения. Стоп с названной причиной, разведка идёт следующим прогоном.
сборщика, переезд гейта на другой инструмент) **или сработал триггер ADR**.
Перечень триггеров не пересказывается: он живёт в
[canon.md](../../canon/references/canon.md#adr), и здесь он работает
стоп-признаком — то есть от его точности зависит выбор сценария, а пересказ
расходится с домом молча. Стоп с названной причиной, разведка идёт следующим
прогоном.
**Последний исход — не редкость, и его стоит ждать.** Незнакомое обслуживание —
это выбор подхода с ценой и с тем, что становится невозможным, — предмет чекпоинта
@@ -223,6 +231,14 @@ ADR: список источников канон закрыл двумя — а
Код и конфиги — по конвенциям проекта. Правка по размеру задачи: чинится названное в записи, соседнее не улучшается
заодно.
**Гейта ещё нет — сказать это, а не изображать сверку.** Первые шаги плана
стройки заводят гейт, сборку и хуки: у них нет ни «до», ни «прежнего», и
определение сделанного через зелёный гейт на них не выполнимо буквально. Такая
задача сделана, когда **заведённое работает на чистом клоне** и это показано в
докладе; пункты 1 и 3 определения ниже закрываются строкой «заводится впервые,
сверять не с чем». Изображать сверку с несуществующим прежним состоянием нельзя —
это ровно то враньё, против которого весь абзац ниже и написан.
**Гейт правится — сверь состав, а не цвет.** Красный, ставший зелёным, виден
сразу; убыли проверок гейт не покажет — он зелёный и до, и после. Это самая
дорогая из возможных правок оснастки: молча выключено то, чем проверяется всё
@@ -242,7 +258,9 @@ ADR: список источников канон закрыл двумя — а
**Поведенческая верификация здесь другая, чем в решении.** Проверяется не новое
поведение, а то, что прежнее не поехало: команда из `CLAUDE.md` поднимается, шаг
сборки отрабатывает, хук ставится на чистом клоне. Правка, которую нельзя
сборки отрабатывает, хук ставится на чистом клоне. **У оснастки, заводимой
впервые, прежнего нет** — тогда проверяется, что заведённое работает на чистом
клоне, и в докладе это называется своим именем, а не «регрессий не найдено». Правка, которую нельзя
проверить ничем, кроме «у меня локально работает», называется в докладе строкой.
### 4. Ревью — план фиксирован сценарием
@@ -259,12 +277,16 @@ Change ты не передаёшь — его нет.
Поэтому план у сценария **свой и постоянный**, и глубину он называет сам —
проходы берут её из метки, а метки здесь нет:
<!-- дом: план-без-метки -->
| Тема | Дом | Кто закрывает | Глубина и вход | Когда |
| --- | --- | --- | --- | --- |
| `autotests` | `CLAUDE.md`, семантика гейта | `review-autotests` | как обычно: гейт запускается целиком | всегда |
| `operations` | `architecture.*`, раздел эксплуатации | `review-basics` | **сверка**: дом темы против диффа, потолок 2 | всегда |
| `conventions` + технический разбор | `conventions.*` | `review-code` | вход `small`: только индекс конвенций; потолки 3 технических и 2 конвенционных; **третья половина включена** — сверка с инвариантами `CLAUDE.md`, потолок 1 | дифф трогает код, а не только оснастку |
<!-- /дом: план-без-метки -->
**Глубина названа в плане потому, что иначе её неоткуда взять.** Вход и потолки
`review-code` заданы меткой, у `review-basics` меткой задана и сама возможность
запуска; на прогоне без метки оба взяли бы их наугад — то есть по-разному от
@@ -324,9 +346,9 @@ Change ты не передаёшь — его нет.
**`adr/` этот сценарий не пополняет, и это не пропуск.** У ADR закрытый список
источников — архивный `design.md` либо записка разведки, — и ни того ни другого
обслуживание не производит. Поэтому триггеры ADR здесь работают **стоп-признаком,
а не поводом завести запись**: сработал дорогой откат, намеренный отказ от
очевидного подхода или пересмотр прежнего решения — сценарий выбран неверно,
объявляй исход **нужна разведка** и останавливайся. Решение с ценой обязано
а не поводом завести запись**: сработал любой из них — сценарий выбран неверно,
объявляй исход **нужна разведка** и останавливайся. Перечень триггеров — в
[canon.md](../../canon/references/canon.md#adr) и здесь не пересказывается. Решение с ценой обязано
пройти чекпоинт вариантов, а не появиться в коммите обслуживания, чей смысл —
«ничего не решали, поменяли оснастку».
@@ -103,10 +103,10 @@ git и читается диффом, а второй стоп на каждой
ходу. Исключение ровно одно и оно не про изменение системы: одноразовый
**читающий** прогон (запрос, замер, скрипт в песочнице), чей результат уезжает
в ответ с провенансом и который ничего не оставляет в репозитории.
- **Приоритетом.** Заведённая задача встаёт в конец своей секции; где ей стоять
в очереди, решает человек на груминге (`av-dev:task-groom`). Разведка, сама
ставящая свой исход первым в беклоге, назначает приоритет тому, что только что
придумала.
- **Местом в списке.** Заведённая задача встаёт в конец своей секции; куда её
поставить, решает человек на доработке грумингом (`av-dev:task-groom`), на
стройке сразу же, по зависимости. Разведка, сама ставящая свой исход первым,
назначает место тому, что только что придумала.
- **Форматом задач и документов.** Индексы и документы руками не правятся: их
ведут `av-dev:task-track` и `av-dev:doc-sync`. Твоё — содержание ответа, их —
форма и дом.
@@ -227,9 +227,14 @@ git и читается диффом, а второй стоп на каждой
отвергнутое (отвергнутый вариант с названной причиной — половина будущего ADR);
приносить один вариант и называть это выбором.
Проверка на простой язык та же, что у чекпоинта решения: **в тексте нет `SHALL`,
нет имён файлов и функций там, где вещь называется по-русски, и нет слов, которых
нет в паспорте проекта.**
Проверка на простой язык — общая у трёх сценариев:
<!-- копия: чекпоинт-простой-язык из av-dev/skills/code-resolve/references/solve.md -->
**в тексте нет `SHALL`, нет имён файлов и функций там, где вещь называется
по-русски, и нет слов, которых нет в паспорте проекта.**
<!-- /копия: чекпоинт-простой-язык -->
Исходы чекпоинта:
@@ -255,8 +260,8 @@ git и читается диффом, а второй стоп на каждой
- **ответ на вопрос** — по адресу из шага 1;
- **отвергнутые варианты с причинами.** Это половина будущего ADR и единственная
защита от повторной разведки того же самого;
- **решение с ценой — в ADR**, если оно проходит триггер канона (дорогой откат,
намеренный отказ, пересмотр прежнего решения). У разведки, кончившейся без
- **решение с ценой — в ADR**, если оно проходит [триггер
канона](../../canon/references/canon.md#adr). У разведки, кончившейся без
кода, `design.md` не будет никогда, поэтому здесь ADR цитирует **записку
разведки**, а не архивный change; канон это допускает прямо, и в записи
источник называется.
+11 -4
View File
@@ -195,10 +195,17 @@ flowchart TD
накопленные до этого места, и находки ревью с пометкой `развилка`;
- **что дальше**, если возражений нет.
Проверка на «простой язык» одна и механическая: **в тексте нет `SHALL`, нет имён
файлов и функций там, где вещь называется по-русски, и нет слов, которых нет в
паспорте проекта.** Не проходит — переписывай, а не объясняй, почему иначе
нельзя.
Проверка на «простой язык» одна и механическая, и она общая у чекпоинтов всех
трёх сценариев — поэтому её дом здесь, а у соседей помеченные копии:
<!-- дом: чекпоинт-простой-язык -->
**в тексте нет `SHALL`, нет имён файлов и функций там, где вещь называется
по-русски, и нет слов, которых нет в паспорте проекта.**
<!-- /дом: чекпоинт-простой-язык -->
Не проходит — переписывай, а не объясняй, почему иначе нельзя.
Длина — экран. Длинное объяснение не читают, а пролистывают, и чекпоинт
превращается в ритуал одобрения.
+30 -14
View File
@@ -59,12 +59,20 @@ description: "Конвейер ревью изменения, устроенны
сценарий обслуживания зовёт конвейер без change и без дельта-спек, и ни один
проход его плана на них не завязан. См. «Прогон без change».
- **Документы канона** — см. следующий раздел.
- **Проектные копии этих скиллов и агентов удаляются при установке.** Если в
проекте уже лежат свои `.claude/skills/review`,
`.claude/skills/review-pipeline`, `.claude/skills/task-pipeline`,
`.claude/skills/task-batch`, `.claude/skills/resolve` или
`.claude/agents/<проект>-review-*.md` — снеси их. Иначе короткое имя разрешится
в устаревшую проектную копию, молча и без признаков подмены.
- **Проектные копии этих скиллов и агентов удаляются при установке.**
<!-- копия: проектные-копии из README.md -->
Голые имена в `.claude/skills/`: `resolve`, `review`, а у проектов прошлого
поколения ещё `task-pipeline`, `review-pipeline`, `task-batch`. С префиксом
проекта: `<проект>-task-pipeline`, `<проект>-review-pipeline`. Агенты:
`.claude/agents/<проект>-review-*.md`.
Две копии одного скилла расходятся, и побеждает та, что **короче названа**:
короткое имя разрешится в устаревшую проектную копию молча и без признаков
подмены.
<!-- /копия: проектные-копии -->
### Чего может не быть
@@ -335,6 +343,8 @@ charter'а, а модель потом двигает калибровка, и
Ревью кода:
<!-- дом: тема-метка-глубина -->
| Тема | `small` | `medium` | `large` |
|---|---|---|---|
| `requirements` | `specs`, сверка | `specs`, разбор | `specs`, разбор |
@@ -345,6 +355,8 @@ charter'а, а модель потом двигает калибровка, и
| `operations` | `code`, сверка по инвариантам | `basics`, разбор | `ops`, доказательство |
| тема проекта | `basics`, сверка | `basics`, разбор | `basics`, разбор |
<!-- /дом: тема-метка-глубина -->
Весь процесс с выбором исполнителей на каждом этапе — одной схемой. **Метка
считается один раз, в узле разметки, и дальше только читается:**
@@ -452,8 +464,8 @@ flowchart TD
— и очередь между ними была бы платой ни за что.
Рёбер два вида, и они разной природы. Путать их нельзя: первое про
**осмысленность** (без плана задание не определено, на красном гейте проход с мнением
проход не о чем), второе про **железо**.
**осмысленность** (без плана задание не определено, а на красном гейте проходу с
мнением не о чем судить), второе про **железо**.
| Ребро | Смысл | Между кем |
|---|---|---|
@@ -693,11 +705,15 @@ change**: у работы, не меняющей поведения, дельт
называет глубину и вход каждого прохода** — их обычный источник метка, и без неё
проходы взяли бы их наугад:
| Тема | Кто закрывает | Глубина и вход | Когда |
|---|---|---|---|
| `autotests` | `review-autotests` | как обычно | всегда |
| `operations` | `review-basics` | сверка, потолок 2 | всегда |
| `conventions` + технический разбор | `review-code` | вход `small` (индекс конвенций), потолки 3 и 2, третья половина включена — потолок 1 | дифф трогает код |
<!-- копия: план-без-метки из av-dev/skills/code-resolve/references/maintain.md -->
| Тема | Дом | Кто закрывает | Глубина и вход | Когда |
| --- | --- | --- | --- | --- |
| `autotests` | `CLAUDE.md`, семантика гейта | `review-autotests` | как обычно: гейт запускается целиком | всегда |
| `operations` | `architecture.*`, раздел эксплуатации | `review-basics` | **сверка**: дом темы против диффа, потолок 2 | всегда |
| `conventions` + технический разбор | `conventions.*` | `review-code` | вход `small`: только индекс конвенций; потолки 3 технических и 2 конвенционных; **третья половина включена** — сверка с инвариантами `CLAUDE.md`, потолок 1 | дифф трогает код, а не только оснастку |
<!-- /копия: план-без-метки -->
Триаж обязателен и здесь — он единственный сток и единственный, кто сверяет план
с исходом; на его вход подаётся этот план вместо плана разметки. Тема
@@ -747,7 +763,7 @@ change**: у работы, не меняющей поведения, дельт
Два прохода, оба против **записанного** критерия. Машину не держат ни один, ребра
между ними нет — уходят одним сообщением сразу после зелёного гейта, вместе со
стадией 3 или 4 — той, что в метки.
стадией 3 или 4 — той, которую назначила метка.
- `review-specs` закрывает тему `requirements`. Критерий взят из **дельта-спек
предлагаемого изменения**, а не из proposal, сообщения коммита или описания
@@ -5,20 +5,26 @@
его достаточно. Здесь то, что читают, когда метку **выбирают, оспаривают или
калибруют**.
Применяет правило `review-scope` при разметке задачи — не автор изменения. Его
рабочая выжимка лежит в уставе агента; расходиться она с этим файлом не вправе, а
при расхождении прав этот.
Применяет правило `review-scope` при разметке задачи — не автор изменения. Сама
матрица уехала в его устав **помеченной копией**, и дословность её держит
`copies.py`, а не обещание: прежде здесь стояло «расходиться не вправе», и
подкреплено это было ничем. Проза вокруг матрицы — отрицательный тест `small`,
доли, цена — принадлежит месту и живёт только здесь.
## Правило выбора — две оси, а не один вопрос
**Оси две, они измеряют разное, и метка есть максимум по ним.**
<!-- дом: матрица-метки -->
| | **знакомое** — форму решения можно назвать до начала | **незнакомое** — форму предстоит нащупать по ходу |
|---|---|---|
| **малое** — один узел | `small` | `large` |
| **среднее** — несколько узлов одного слоя | `medium` | `large` |
| **крупное** — несколько слоёв, перенос ответственности, большой рефакторинг | `large` | `large` |
<!-- /дом: матрица-метки -->
**Метка — не синоним размера.** Совпадают они только в левом верхнем углу: малое
**незнакомое** изменение получает `large`, трогая один узел. Поэтому в плане
стоят три строки, а не одна: размер, сложность и метка — каждая со своим
+1 -1
View File
@@ -36,7 +36,7 @@ description: Вести содержимое документов канона
| `openspec/specs/` | всегда при изменении поведения | вливает `opsx:archive` |
| `database.md` | тронуты миграции | `docs.py check --base` |
| `architecture.md` | новый компонент, граница, внешняя зависимость, изменилось окружение | `docs.py`: capability без упоминания |
| `adr/` | дорогой откат, намеренный отказ, пересмотр прежнего | нет — только этот чек-лист |
| `adr/` | сработал триггер канона (перечень — [canon.md](../canon/references/canon.md#adr)) | нет — только этот чек-лист |
| `research/` | узнали новое о внешнем формате или данных | нет |
| `security.md` | новый недоверенный вход, токен, путь наружу, сдвиг периметра | нет |
| `conventions/` | находка принята и не специфична для одного места | промоут |
+18 -8
View File
@@ -53,11 +53,19 @@ description: "Груминг беклога — интерактивный ра
не 5–8 задач из середины. Порционный разбор здесь вреден: он вынимает шаги из
списка, порядок которого и есть его содержание.
Поэтому на стройке скилл говорит это строкой и **предлагает другую работу**:
пересмотр плана целиком, гигиену полей (`task-track`) или переход в доработку,
если беклог исчерпан. Три вещи он делает и там, потому что от стадии они не
зависят: `tasks.py check --fix`, разбор накопившихся вопросов и закрытие того,
что сделано попутно.
Поэтому на стройке скилл говорит это строкой и **отсылает к другой работе**:
[пересмотр плана целиком](../task-track/SKILL.md#пересмотр-плана-стройки) —
сценарий скилла `task-track`, гигиена полей — тоже его, а исчерпанный беклог
значит переход (`tasks.py stage support`). Четыре вещи он делает и на стройке,
потому что от стадии они не зависят: `tasks.py check --fix`, разбор
накопившихся вопросов, закрытие сделанного попутно и **возврат неудавшейся
приёмки** (`reopen`).
**Возврат приёмки от стадии не зависит вовсе, и это надо сказать отдельно.**
Приёмщик и исполнитель у нас совпадают, и опор против этого две: независимый
отчёт ревью и `reopen`. Вторая привязана к грумингу только по привычке — заметил,
что закрытая задача сделана не тем, чем обещала, возвращай сразу, на любой
стадии и в любой момент.
## Когда груминг созрел
@@ -69,6 +77,8 @@ description: "Груминг беклога — интерактивный ра
- на верхних строках очереди есть задача с открытым вопросом — очередь
показывает то, что взять нельзя;
- `tasks.py check` печатает «готово к взятию: 0 из N» — брать сегодня нечего.
**На стройке этот признак читается иначе**: он значит «план ещё не дописан», а
не «пора грумить».
Порога в неделях нет намеренно: счётчик простоя пришлось бы вести руками, а
решает всё равно человек. Признак — **очередь перестала быть твоей**: взялся
@@ -215,9 +225,9 @@ flowchart TD
- **независимый отчёт ревью** — артефакт, написанный не исполнителем: отчёт
триажа в
`openspec/changes/archive/<id>/review/` (до архивации — `changes/<id>/review/`);
- **`reopen <слаг> --reason`** — закрытие не окончательно. Заметил на груминге,
что закрытая задача сделана не тем, чем обещала, — возвращай, это штатная
операция, а не скандал. Индексы под git: `git log -p` по беклогу показывает,
- **`reopen <слаг> --reason`** — закрытие не окончательно. Заметил, что закрытая
задача сделана не тем, чем обещала, — возвращай, это штатная операция, а не
скандал, и ждать груминга она не требует (на стройке его и не будет). Индексы под git: `git log -p` по беклогу показывает,
что и когда закрыто, потому что закрытие коммитится отдельным коммитом учёта.
Известные обходы:
+33 -3
View File
@@ -190,7 +190,9 @@ stateDiagram-v2
Вместе с целью ушли: секция `Готово` (её ответ дают спеки и история), теги
`goal:<слаг>` и `decomposed`, поле меты `Секция` (осталась `Категория`), раздел
`Завершение` и команды `list --goal`, `edit --goal`. Встретились в проекте —
`Завершение`, ключи `[tasks] roadmap` и `[tasks] completion_heading`, флаги
`add --goal`, `list --goal`, `list --index`, `edit --goal`, `edit --section` и
`init --roadmap`. Встретились в проекте —
`check` назовёт их поимённо, `check --fix` снимет теги, а запись типа `goal`
оставит человеку: во что она превращается — в задачу или в ничто, — машина не
решает.
@@ -517,7 +519,9 @@ python3 $tk adopt scan --from … --stage S | apply --plan … # разова
пять файлов из одной мысли, а сорок файлов из сорока сырых находок. Защита та
же, что в самом ревью: кластеризация по причине, дедуп против живых и
`REJECTED.md`, находка без свидетельства → сырьё (`research`), а не задача, и карта кластеров
пользователю до создания файлов. Порядок и отображение серьёзности —
пользователю до создания файлов. **Стадию смотри и здесь**, тем же нулевым
шагом: от неё зависит, куда ляжет тяжёлая находка — наверх очереди или после
своей зависимости. Порядок и отображение серьёзности —
[references/from-review.md](references/from-review.md).
### Прийти в репозиторий, где задачи уже как-то ведутся
@@ -530,6 +534,32 @@ python3 $tk adopt scan --from … --stage S | apply --plan … # разова
Если переводить надо не только задачи, а весь `docs/` — это скилл
`av-dev:canon`, и он зовёт этот сценарий сам на своём шаге.
### Пересмотр плана стройки
Операция стадии `build`, и на доработке её нет: там переоценка идёт порциями и
называется грумингом. **Повод один — сменился замысел**, а не «давно не
смотрели»: план стройки протухает не по частям, а целиком, потому что порядок в
нём — зависимость, и одна изменившаяся посылка переставляет всё, что ниже.
Порционный разбор здесь вреден, и это не вкус: вынуть пять шагов из середины
списка, порядок которого и есть его содержание, — значит получить план, про
который никто уже не скажет, почему он такой.
1. **Назови, что изменилось в замысле.** Одной фразой, и она уедет причиной в
каждое движение. Не находится — значит повода нет, и пересмотр не нужен.
2. **Прочитай список целиком**, сверху вниз, и по каждой строке ответь одно из
трёх: остаётся как есть, переезжает (`move --after` с причиной), уходит
(`close --reason`). Дописанное новое встаёт туда, куда велит зависимость, а
не в конец.
3. **Покажи человеку весь новый список**, а не отдельные решения: план читается
только целиком. `AskUserQuestion` с готовым порядком и доводом на каждое
движение.
4. `check` и доклад: сколько строк тронуто из скольких, что ушло и почему.
**Границу с грумингом держи твёрдо.** Если хочется пересмотреть план «потому что
накопилось» — это не пересмотр, а признак того, что стройка кончилась: беклог
перестал быть планом и стал очередью. Проверь `stage`.
### Декомпозиция и штурм сырья
[references/split.md](references/split.md). Обе операции превращают одну запись в
@@ -557,7 +587,7 @@ python3 $tk adopt scan --from … --stage S | apply --plan … # разова
одну половину делает дорогой, а вторую — поверхностной.
Модель у обоих одна, `sonnet`, и это не отменяет разреза. Оба судят по
**записанному правилу** — семь пунктов формы против правил языка, — а их находка
**записанному правилу** — шесть пунктов формы против правил языка, — а их находка
приходит готовой формулировкой, которую читает и отклоняет человек, а не молча
реализует оркестратор. Ошибка здесь стоит строки чтения, и платить за неё верхней
моделью не за что.
+4 -3
View File
@@ -100,9 +100,10 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
`apply` печатает состояние по факту: сколько задач не собрало разделы своего типа
(для `check` это не ошибка, а строка здоровья, но `ready` такую задачу не
пропустит). Закрывается это **порциями груминга** — скилл `groom`, 58 задач за
порцию: превратить «готово, когда» в критерии с оракулами, вынуть вопросы из
прозы в раздел «Вопросы».
пропустит). Закрывается это **порциями по 58 задач**: превратить «готово,
когда» в критерии с оракулами, вынуть вопросы из прозы в раздел «Вопросы». На
доработке это груминг (скилл `av-dev:task-groom`), на стройке — гигиена полей
этого скилла: груминга там нет.
**Порядок строк проверяется глазами отдельно.** На стройке он выведен из
нумерации источника, и там, где её не было, он случаен. На доработке машина
@@ -83,6 +83,14 @@
[скилле груминга](../../task-groom/SKILL.md#приоритет-как-его-расставляют), и
серьёзность попадает ровно в один из них.
**Всё это — про доработку.** На стройке порядок строк значит зависимость, и
`--first` там означает «ни от чего не зависит», а не «важнее всех»: находка,
поднятая наверх, встанет перед собственной зависимостью. Место находке на стройке
называет зависимость — `move --after <шаг, после которого её можно делать>`, — а
серьёзность идёт **причиной в мете** и разбирается ближайшим пересмотром плана
(`task-track`, «Пересмотр плана стройки»). Груминга там нет, и откладывать «до
него» некуда.
- **тяжёлая находка со свидетельством о сломанном сейчас** → задача
**первой строкой секции**: `move <слаг> --first
--reason «сломано сейчас: …»`. Это довод «что сломано сейчас» из перечня
@@ -110,10 +110,13 @@
| поле **Хук** | поле **Зачем** |
| мета одной строкой через `·` | мета списком, поле на строку |
Единственное, чего `--fix` не делает сам, — **проставить тип записи, у которой
его неоткуда взять**: `feature` от `chore` машина не отличает, и подставленное
наугад значение врало бы ровно там, где по нему принимают решение. Такие записи
он называет поимённо пометкой `НЕОДНОЗНАЧНО`.
Чего `--fix` не делает сам — **решает за человека, каким быть типу**. Случаев
три, и все три уезжают пометкой `НЕОДНОЗНАЧНО`: тип, которого неоткуда взять
(`feature` от `chore` машина не отличает); тип вне словаря; и запись типа `goal`
— целей больше нет, а во что превращается эта, в задачу или в ничто, машина не
знает. Подставленное наугад значение врало бы ровно там, где по нему принимают
решение. Мёртвые теги и имя поля места при этом снимаются у **любой** записи,
включая ту, чей тип остался неразобранным.
### Затрагивает
@@ -42,7 +42,8 @@
| Место в секции беклога | **конец**, `check --fix` сносит туда сам | среди прочих |
| `tasks.py list --raw` | показывает | нет |
Порядок строк в беклоге назначает человек — это приоритет (правило 4 скилла).
Порядок строк в беклоге назначает человек, и стадия решает, что он значит:
зависимость на стройке, важность на доработке (правило 4 скилла).
Место сырья **из него изъято**: оно производно от типа и заполненности, а не от
чьего-то решения, и потому его проверяет и чинит машина. Приоритетом оно не
становится: сырьё не берут вовсе, и место в конце говорит именно это.
+324 -125
View File
@@ -189,6 +189,16 @@ DEFAULTS = {
# с диском первым делом, иначе кривой ключ выглядит как пропавший файл).
PATH_KEYS = ("items", "backlog", "rejected")
# Упразднённые части каталога — с адресом, куда уехало содержимое. Перечень
# читает `scripts/addresses.py`: адрес, названный в чужой прозе, опровергается
# перечнем владельца, а не памятью. Без этой константы упразднение, сделанное
# здесь, не ловилось бы гейтом вовсе — то есть шаг гейта молчал бы ровно про то,
# ради чего заведён.
RETIRED = {
"ROADMAP.md": "→ BACKLOG.md: роадмап упразднён вместе с типом goal",
"SPRINT.md": "→ порядок строк беклога (спринты отменены)",
}
# Умолчания секций по стадиям. На доработке это **полки домена**: смысла они не
# несут, называет их проект. На стройке секция ровно одна — список от базы к
# деталям, — и её имя тоже дело проекта: различать ей нечего, она одна.
@@ -262,6 +272,10 @@ BULLET = re.compile(r"^[-*]\s+(.*)$")
REJECTED_ENTRY = re.compile(r"^- \d{4}-\d{2}-\d{2} `[a-z0-9-]+` — .+")
RESEARCH = "research"
# Прежний дом направления. Тип `goal` упразднён вместе с роадмапом; читается
# только затем, чтобы `check` назвал его вслух, а `--fix` снял его следы.
LEGACY_GOAL_TAG = "goal:"
LEGACY_GOAL = "goal"
# Тип — ось записи и **закрытый словарь**. Открытый разъедется на синонимах
# (`bug`, `bugfix`, `fix`, `defect`), и отбор по типу перестанет отвечать на
# свой единственный вопрос. Ни один тип не подходит — это сигнал, что в записи
@@ -272,17 +286,16 @@ TYPES = ("feature", "fix", "chore", RESEARCH)
# дословно, — так тип виден там, где решают «брать или не брать», и инвариант
# «заголовок в индексе дословно» остаётся нетронутым.
TYPE_EMOJI = {"feature": "", "fix": "🐞", "chore": "🧹", RESEARCH: "🔬"}
EMOJI_TYPE = {v: k for k, v in TYPE_EMOJI.items()}
# Эмодзи упразднённого типа читается по-прежнему — иначе она не **снимается**:
# заголовок разбирается на «значок + текст», и незнакомый значок уезжает в текст,
# а следующая правка типа ставит второй перед первым («✨ 🎯 …»).
EMOJI_TYPE = {v: k for k, v in TYPE_EMOJI.items()} | {"🎯": LEGACY_GOAL}
TAKEABLE = TYPES # берутся в работу все четыре: целей больше нет
# Заголовок в форме действия требуется там, где исход работы — изменение
# системы. У разведки он называет предмет: её исход знание, и заголовок-действие
# обещал бы решённость, которой ещё нет.
ACTION_TYPES = ("feature", "fix", "chore")
QUESTION_TAG = "question"
# Прежний дом направления. Тип `goal` упразднён вместе с роадмапом; тег
# читается только затем, чтобы `check` назвал его вслух, а `--fix` снял.
LEGACY_GOAL_TAG = "goal:"
LEGACY_GOAL = "goal"
# Схема тела на тип: какие разделы обязательны, какие ещё допустимы. Значения —
# **ключи конфига**, а не сами заголовки: имена заголовков проект настраивает,
@@ -458,7 +471,12 @@ class Layout:
# Стадия — не имя части, поэтому и не в `cfg`. Пустая строка значит «не
# объявлена», и это отдельное состояние: без неё непонятно, что значит
# порядок строк, и `check` об этом говорит.
self.stage = cfg.get(STAGE_KEY, "")
#
# Приводится к нижнему регистру ровно потому, что к нему же приводит
# валидация: `stage = "Build"` проходил её и не совпадал ни с одним
# значением здесь, так что каждое ветвление молча уходило в ветку
# «стадии нет» — при зелёном конфиге и объявленной стадии.
self.stage = str(cfg.get(STAGE_KEY, "")).strip().lower()
def index(self, kind: str) -> Path:
return self.root / self.cfg[kind]
@@ -1145,6 +1163,7 @@ def check(lay: Layout, fix: bool = False) -> int:
raw_names = {n for n, t in tasks.items() if raw_research(lay, t)}
errors += sections_verdict(lay, sections, label)
errors += stage_block_verdict(lay, lines, label)
for s in sections:
if s.lower() in BLOCKER_SECTIONS:
notes.append(f"{label}: секции «{s}» быть не должно —"
@@ -1457,6 +1476,12 @@ def list_tasks(lay: Layout, a: argparse.Namespace) -> int:
for t in rows:
t["touched"] = dates.get(t["path"].name, "")
rows.sort(key=lambda t: (t["touched"] == "", t["touched"]))
# Отбор работает на любой стадии, но значит он разное, и молчать об этом
# нельзя: на стройке шаг лежит долго законно — до него не дошла очередь,
# и он стоит там, где стоит, по зависимости.
if lay.stage == BUILD:
print(" стройка: залежалость здесь не мера — шаг ждёт своей"
" очереди по зависимости, а не потому, что его обходят\n")
else:
rows.sort(key=lambda t: (order.get(t["section"], 99), t["path"].name))
@@ -1700,8 +1725,8 @@ def cmd_add(lay: Layout, a: argparse.Namespace) -> int:
section = a.section or (section_headers(lines)[0][1] if section_headers(lines) else "")
hi, section = find_section(lines, section)
if hi is None:
avail = ", ".join(n for _, n in section_headers(lines))
raise Usage(f"нет секции «{a.section}» в {lay.name('backlog')} (есть: {avail})")
avail = ", ".join(n for _, n in section_headers(lines)) or "ни одной"
raise Usage(f"нет секции «{section}» в {lay.name('backlog')} (есть: {avail})")
tags = split_tags(a.tag)
if QUESTION_TAG in tags:
@@ -1766,9 +1791,6 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int:
if not path.exists():
raise Usage(f"{a.slug}.md не найден в {lay.cfg['items']}/")
place = locate(lay, a.slug)
if place is None:
raise Usage(f"строки индекса для {a.slug} нет — прогони check --fix")
task = parse_task(path)
if a.title is not None and not a.title.strip():
raise Usage("пустой заголовок")
@@ -1802,6 +1824,21 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int:
# правкой. Сверка принадлежности всё равно идёт по нижнему регистру.
section = task["section_raw"]
# Строки индекса может не быть, и это не всегда поломка: запись, пережившая
# упразднение своего индекса, лежит файлом без строки, а `check --fix`
# восстановить её не может — секция в мете указывает на исчезнувшую полку.
# Отказ здесь запирал бы такую запись навсегда: строку не восстановить, пока
# не сменишь тип, и тип не сменить, пока нет строки. Заводим строку сами, в
# первую секцию, и говорим об этом.
lost_section = ""
if place is None:
heads = section_headers(read_lines(lay.index("backlog")))
if not heads:
raise Usage(f"строки для {a.slug} нет, и в {lay.name('backlog')} нет"
f" ни одной секции — заводить её некуда")
if find_section(read_lines(lay.index("backlog")), section)[0] is None:
lost_section, section = section or "", heads[0][1]
# Тип передаётся всегда, а не только при `--type`: у файла, не переехавшего
# на поле, он выведен из прежнего дома, и без него пересборка меты потеряла
# бы его вовсе.
@@ -1823,8 +1860,12 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int:
if rtype == RESEARCH and not task["body"].get(lay.cfg["question_heading"].lower()):
raw_now.add(f"{a.slug}.md")
lines, ei = place
lines[ei] = entry_line(lay, h1, a.slug, why)
if place is None:
lines = read_lines(lay.index("backlog"))
insert_entry(lines, section, entry_line(lay, h1, a.slug, why))
else:
lines, ei = place
lines[ei] = entry_line(lay, h1, a.slug, why)
plan = Plan()
plan.file(path, new_text)
@@ -1835,6 +1876,11 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int:
("теги", a.add_tag or a.rm_tag))
if v is not None]
print(f"{a.slug}: обновлено ({', '.join(changed)})")
if place is None:
print(f" строки в {lay.name('backlog')} не было — заведена в секции"
f" «{section}», в конец: позицию назначает человек"
+ (f" (секции «{lost_section}» из меты в индексе нет)"
if lost_section else ""))
if QUESTION_TAG in tags and QUESTION_TAG not in task["tags"]:
print(f" вопрос открыт — в работу задача не берётся, пока он не разобран"
f" (`tasks.py ready {a.slug}` это и скажет)")
@@ -1844,11 +1890,16 @@ def cmd_edit(lay: Layout, a: argparse.Namespace) -> int:
def cmd_move(lay: Layout, a: argparse.Namespace) -> int:
"""Перестановка строки: внутри своей секции или в другую.
**Что значит перестановка, говорит стадия, а не эта команда**: на стройке она
называет зависимость («этот шаг делается после того»), на доработке
приоритет («это берут раньше»). Отсюда и `--reason`: причина у двух движений
разная, и через месяц её не восстановить.
`--section` необязателен, и это не удобство. Перестановка внутри секции
самая частая операция груминга (`move --after` и есть расстановка
приоритета), а требовать в ней повторить текущую секцию значит приглашать
указать не ту: перенос в чужую секцию выглядел бы ровно так же. Без
`--section` секция берётся из индекса та, в которой строка уже лежит.
самая частая операция и там и там, а требовать в ней повторить текущую секцию
значит приглашать указать не ту: перенос в чужую секцию выглядел бы ровно так
же. Без `--section` секция берётся из индекса та, в которой строка уже
лежит.
"""
for err in (bad_slug(a.slug), bad_reason(a.reason), bad_slug(a.after) if a.after else None):
if err:
@@ -1873,6 +1924,15 @@ def cmd_move(lay: Layout, a: argparse.Namespace) -> int:
avail = ", ".join(n for _, n in section_headers(lines))
raise Usage(f"нет секции «{a.section}» в {label} (есть: {avail})")
task = parse_task(path)
# Место сырья производно от типа, а не назначается: назначить его — значит
# получить дрейф, который следующий же `check --fix` отменит, стерев решение
# человека. Поэтому отказ, и с названным выходом: сырьё перестаёт быть
# сырьём, как только у него появляется «Вопрос».
if (a.after or a.first) and raw_research(lay, task):
raise Usage(f"{a.slug} — сырьё (`{RESEARCH}` без раздела"
f" «{lay.cfg['question_heading']}»), и место у него не"
f" назначается: конец секции, потому что его не берут."
f" Допиши «{lay.cfg['question_heading']}» — и переставляй")
new_text = meta_updated(path, section=section, reason=a.reason,
rtype=task["type"] or None)
if new_text is None:
@@ -1883,9 +1943,12 @@ def cmd_move(lay: Layout, a: argparse.Namespace) -> int:
insert_entry(lines, section, entry, a.after, a.first)
except KeyError as e:
raise Usage(f"--after {a.after}: такой строки в секции «{section}» нет") from e
if not (a.after or a.first):
lines[:] = raw_last(lines, {n for n, t in tasks_of(lay).items()
if raw_research(lay, t)})
# Сырьё сносится в конец **всегда**, в том числе после `--after`/`--first`:
# переставленная строка от этого не двигается (она не сырьё — отказ выше),
# а вот сырьё, оказавшееся выше неё, встаёт на своё место сразу, а не до
# ближайшего `check --fix`.
lines[:] = raw_last(lines, {n for n, t in tasks_of(lay).items()
if raw_research(lay, t)})
plan = Plan()
plan.file(path, new_text)
@@ -1905,9 +1968,10 @@ def cmd_close(lay: Layout, a: argparse.Namespace) -> int:
path = lay.items / f"{a.slug}.md"
if not path.exists():
raise Usage(f"{a.slug}.md не найден в {lay.cfg['items']}/")
# Строки может не быть, и отказ здесь запирал бы запись навсегда: чтобы
# строку восстановить, надо её куда-то класть, а класть незачем — запись
# закрывают. Так закрывается и то, что пережило упразднение своего индекса.
place = locate(lay, a.slug)
if place is None:
raise Usage(f"строки индекса для {a.slug} нет — прогони check --fix")
task = parse_task(path)
plan = Plan()
@@ -1922,13 +1986,16 @@ def cmd_close(lay: Layout, a: argparse.Namespace) -> int:
if not prev.endswith("\n"):
prev += "\n"
plan.file(rej, prev + bullet + "\n")
lines, ei = place
lines.pop(ei)
plan.index(lay, "backlog", lines)
if place is not None:
lines, ei = place
lines.pop(ei)
plan.index(lay, "backlog", lines)
plan.delete(path)
plan.commit()
print(f"{a.slug}: {'записано в ' + lay.name('rejected') + ' + удалено' if a.reason else 'удалено (реализовано, есть коммит)'}")
if place is None:
print(f" строки в {lay.name('backlog')} не было — удалён только файл")
if not a.reason:
print(" дорога назад: файл восстанавливается из git —"
f" `tasks.py reopen {a.slug} --reason «приёмка не сошлась: …»`")
@@ -1987,19 +2054,32 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int:
else:
text = upd
tmp = parse_task_text(text, path)
plan = Plan()
plan.file(path, text)
# Написание — из меты как есть: имя секции уедет в доклад, а сверка
# принадлежности всё равно идёт по нижнему регистру (`find_section`).
section = tmp["section_raw"] or tmp["section"]
lines = read_lines(lay.index("backlog"))
heads = section_headers(lines)
lost_section = ""
if find_section(lines, section)[0] is None:
# Секции могло не стать законно: смена стадии переразмечает беклог, и
# задача, закрытая до перехода, ссылается на исчезнувшую полку. Отказ
# здесь означал бы, что закрытое до перехода не возвращается никогда, —
# а `reopen` заведён ровно на случай, когда приёмка не сошлась. Кладём в
# первую секцию, правим мету и говорим об этом вслух.
if not heads:
raise Usage(f"в {lay.name('backlog')} нет ни одной секции —"
f" возвращать некуда")
lost_section, section = section, heads[0][1]
rebuilt = meta_rebuilt(text.splitlines(), section=section)
if rebuilt is not None:
text = "\n".join(rebuilt) + "\n"
tmp = parse_task_text(text, path)
plan = Plan()
plan.file(path, text)
if find_entry_index(lines, a.slug) is None:
hi, sec = find_section(lines, section)
if hi is None:
avail = ", ".join(n for _, n in section_headers(lines))
raise Usage(f"секции «{section}» нет в {lay.name('backlog')} (есть: {avail})")
insert_entry(lines, sec, entry_line(lay, title, a.slug, tmp["why"]))
insert_entry(lines, section, entry_line(lay, title, a.slug, tmp["why"]))
# Место сырья производно от типа, и `reopen` обязан его соблюсти сразу:
# вернуть разведку без «Вопроса» просто в конец секции — значит
# поставить её после сырья, лежавшего там раньше, и получить ошибку
@@ -2028,8 +2108,11 @@ def cmd_reopen(lay: Layout, a: argparse.Namespace) -> int:
# а его назначает человек. Молча вернуть задачу наверх очереди значило бы
# принять за него решение, которого он не принимал.
print(f"{a.slug}: возвращён в {lay.name('backlog')} из истории git"
f" (секция «{section}», в конец: позиция это приоритет, и её"
f" назначает человек)")
f" (секция «{section}», в конец: позицию назначает человек)")
if lost_section:
print(f" секции «{lost_section}» в беклоге больше нет — положен в"
f" «{section}», «{PLACE_KEY}» в файле поправлена. Так бывает после"
f" смены стадии: состав секций там переразмечается")
for line in removed:
print(f" снята строка {lay.name('rejected')}: {line.strip()}")
print(" сверь тело: оно восстановлено на момент удаления, всё позднейшее"
@@ -2181,6 +2264,37 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
# принимают решение.
for name, task in tasks.items():
rtype = task["type"]
# Мёртвые теги и прежнее имя поля места снимаются у **любой** записи,
# включая ту, чей тип машина исправить не берётся. Тег `goal:` мёртв
# независимо от того, что за запись его несёт, а «Секция» — прежнее имя
# того же самого поля. Стой эта правка после разбора типа, перевод
# проекта оставлял бы их ровно в файлах целей — то есть в тех, которые
# человек как раз и разбирает руками, и разбирал бы он их с мусором.
want_key = PLACE_KEY.lower()
tags = [t for t in task["tags"]
if not t.startswith((LEGACY_KIND_TAG, LEGACY_GOAL_TAG))
and t != LEGACY_DECOMPOSED_TAG]
renamed = bool(task["place_key"]) and task["place_key"] != want_key
retyped = bool(rtype) and rtype in TYPES and task["meta_type"] != rtype
if tags != task["tags"] or renamed or retyped:
if not stage(task, rtype=rtype if retyped else None,
tags=tags if tags != task["tags"] else None):
ambiguous.append(f"{name}: чинить мету некуда — в файле нет"
f" мета-блока")
else:
what = []
if retyped:
what.append(f"тип «{rtype}» в поле **Тип:**")
if task["legacy_kind"]:
what.append(f"снят тег {LEGACY_KIND_TAG}{task['legacy_kind']}")
if task["legacy_goal"]:
what.append(f"снят тег {task['legacy_goal']}")
if LEGACY_DECOMPOSED_TAG in task["tags"]:
what.append(f"снят тег {LEGACY_DECOMPOSED_TAG}")
if renamed:
what.append(f"поле места → «{PLACE_KEY}»")
fixed.append(f"{name}: " + ", ".join(what))
if not rtype:
ambiguous.append(f"{name}: тип не выводится — нет ни поля **Тип:**, ни"
f" тега {LEGACY_KIND_TAG}<род>, ни префикса заголовка."
@@ -2197,29 +2311,6 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
f" ({', '.join(TYPES)}) — чем он заменяется,"
f" решает человек")
continue
want_key = PLACE_KEY.lower()
# Прежние теги снимаются здесь же: у обоих не осталось того, на что они
# указывали, — целей нет, а разбирать нечего.
tags = [t for t in task["tags"]
if not t.startswith((LEGACY_KIND_TAG, LEGACY_GOAL_TAG))
and t != LEGACY_DECOMPOSED_TAG]
renamed = bool(task["place_key"]) and task["place_key"] != want_key
if task["meta_type"] != rtype or tags != task["tags"] or renamed:
if not stage(task, rtype=rtype,
tags=tags if tags != task["tags"] else None):
ambiguous.append(f"{name}: тип «{rtype}» переносить некуда —"
f" в файле нет мета-блока")
else:
what = [f"тип «{rtype}» в поле **Тип:**"]
if task["legacy_kind"]:
what.append(f"снят тег {LEGACY_KIND_TAG}{task['legacy_kind']}")
if task["legacy_goal"]:
what.append(f"снят тег {task['legacy_goal']}")
if LEGACY_DECOMPOSED_TAG in task["tags"]:
what.append(f"снят тег {LEGACY_DECOMPOSED_TAG}")
if renamed:
what.append(f"поле места → «{PLACE_KEY}»")
fixed.append(f"{name}: " + ", ".join(what))
want_h1 = h1_of(rtype, task["bare"])
if task["title"] and task["title"] != want_h1:
src = staged_lines(task)
@@ -2287,11 +2378,14 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
insert_entry(idx[kind], section, entry_line(lay, task["title"], name[:-3],
task["why"]))
# Восстановленная строка встаёт в конец секции, и это надо сказать:
# позиция в беклоге — приоритет, а его назначает человек. Молчаливое
# восстановление выдало бы машинную позицию за его решение.
# позицию назначает человек, а машинная выдала бы себя за его
# решение. Чем именно она была бы — зависимостью или приоритетом, —
# решает стадия, и назвать её тут дешевле, чем заставлять вспоминать.
means = ("зависимость" if lay.stage == BUILD else
"приоритет" if lay.stage == SUPPORT else "порядок работ")
fixed.append(f"{lay.name(kind)}: восстановлена строка {name}"
" — в конце секции, позицию назначь сам:"
" порядок строк это приоритет"
f" — в конце секции, позицию назначь сам:"
f" порядок строк это {means}"
+ ("" if task["why"] else " (в файле нет «зачем» — допиши)"))
dirty.add(kind)
continue
@@ -2311,6 +2405,13 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
# имеет; тем более он не сливает их на стройке — в каком порядке пойдут
# строки слитых полок, знает только человек.
for kind, lines in idx.items():
if kind == "backlog" and lay.stage in STAGES:
span = stage_block_span(lines)
if span is not None and lines[span[0]:span[1]] != stage_block(lay.stage):
lines[span[0]:span[1]] = stage_block(lay.stage)
fixed.append(f"{lay.name(kind)}: шапка переписана под стадию"
f" «{STAGE_RU[lay.stage]}»")
dirty.add(kind)
raw = {n for n, t in tasks.items() if raw_research(lay, t)}
if (moved := raw_last(lines, raw)) != lines:
lines[:] = moved
@@ -2360,9 +2461,50 @@ BACKLOG_ORDER = {
"одной, по мере появления; пустой беклог — нормальное состояние.",
}
# Абзац шапки, объявляющий стадию, размечен парой комментариев — и это не
# украшение. Стадия решает, что значит порядок строк, а читают об этом **здесь**:
# индекс открывают вместо документации. Без разметки `stage` не знал бы, что
# переписывать, и абзац продолжал бы называть прежнюю стадию — молча и навсегда.
# С разметкой расхождение шапки с конфигом становится обычным дрейфом: `check`
# его называет, `check --fix` правит.
STAGE_OPEN = "<!-- стадия -->"
STAGE_CLOSE = "<!-- /стадия -->"
def init_files(lay: Layout, sections: list[str], stage_name: str,
cfg: dict) -> dict[Path, str]:
def stage_block(stage_name: str) -> list[str]:
return [STAGE_OPEN,
f"Стадия проекта — **{STAGE_RU[stage_name]}**"
f' (`[tasks] {STAGE_KEY} = "{stage_name}"`).',
BACKLOG_ORDER[stage_name],
STAGE_CLOSE]
def stage_block_span(lines: list[str]) -> tuple[int, int] | None:
"""Границы размеченного абзаца — `[начало, конец)`. None, если разметки нет."""
try:
start = next(i for i, ln in enumerate(lines) if ln.strip() == STAGE_OPEN)
end = next(i for i in range(start + 1, len(lines))
if lines[i].strip() == STAGE_CLOSE)
except StopIteration:
return None
return start, end + 1
def stage_block_verdict(lay: Layout, lines: list[str], label: str) -> list[str]:
"""Шапка беклога против конфига. Разметки нет — молчим: индекс мог быть
заведён до её появления, и требовать её от чужого файла не за что."""
span = stage_block_span(lines)
if span is None or lay.stage not in STAGES:
return []
want = "\n".join(stage_block(lay.stage))
if "\n".join(lines[span[0]:span[1]]).strip() == want.strip():
return []
return [f"{label}: шапка объявляет не ту стадию, что конфиг"
f" ({STAGE_RU[lay.stage]}) — а читают о смысле порядка строк"
f" именно её; перепишет `check --fix`"]
def init_files(lay: Layout, sections: list[str], stage_name: str) -> dict[Path, str]:
out: dict[Path, str] = {}
# Служебный файл здесь не заводится: его пишет `write_config` по живому
# файлу — версию двигает построчно, ключи дописывает, чужого не затирает.
@@ -2372,9 +2514,7 @@ def init_files(lay: Layout, sections: list[str], stage_name: str,
"# Беклог\n\n"
f"Что **можно взять**. Одна задача = один файл `{lay.cfg['items']}/<slug>.md`\n"
"+ строка здесь. Ведётся скиллом `av-dev:task-track`.\n\n"
f"Стадия проекта — **{STAGE_RU[stage_name]}** (`[tasks] {STAGE_KEY} ="
f' "{stage_name}"`).\n'
+ BACKLOG_ORDER[stage_name] + "\n\n"
+ "\n".join(stage_block(stage_name)) + "\n\n"
f"Одно место в очереди назначено не человеком, а типом: сырьё"
f" (`{RESEARCH}`\nбез раздела «{lay.cfg['question_heading']}») стоит в"
" конце секции — его не берут.\n\n"
@@ -2491,7 +2631,7 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
lay.items.mkdir(parents=True, exist_ok=True)
plan = Plan()
for path, text in init_files(lay, sections, stage_name, cfg).items():
for path, text in init_files(lay, sections, stage_name).items():
plan.file(path, text)
plan.commit()
said = write_config(project, cfg)
@@ -2507,17 +2647,17 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
# --- stage: смена стадии проекта ---
def cmd_stage(lay: Layout, a: argparse.Namespace) -> int:
"""Показать стадию или сменить её.
"""Показать стадию, объявить её впервые или сменить.
Смена событие, а не настройка: она переворачивает смысл порядка строк
(зависимость важность) и состав секций. Записывается она ключом конфига,
и датой ей служит коммит отдельного журнала для одной строки заводить не
стоит.
**Объявление и смена разные операции, и различает их не флаг, а факт:**
была ли стадия названа раньше. Объявление беклога не трогает вовсе оно
называет то, что уже верно, и переразметить при этом чужие полки значило бы
подменить ответ на вопрос вопросом о нём. Смена трогает шапку и конфиг, а
состав секций **только если её об этом попросили** `--sections`.
Остаток беклога переезжает в первую новую секцию **целиком и молча не
остаётся**: строки, писавшиеся как план, на доработке продолжают жить
задачами, но порядок их с этого момента значит другое, и об этом говорится
вслух.
Слить полки сам скрипт не берётся ни в одном из случаев (решение Р240): в
каком порядке пойдут строки слитых полок, знает человек. Отсюда и отказ на
стройке при нескольких секциях с названным выходом, а не глухой.
"""
if a.to is None:
print(f"стадия: {STAGE_RU.get(lay.stage, 'не объявлена')}"
@@ -2528,54 +2668,107 @@ def cmd_stage(lay: Layout, a: argparse.Namespace) -> int:
want = a.to.strip().lower()
if want == lay.stage:
raise Usage(f"стадия уже «{want}» — менять нечего")
sections = uniq_sections(a.sections or DEFAULT_SECTIONS[want])
if not sections:
raise Usage("пустой список секций")
if want == BUILD and len(sections) > 1:
raise Usage(f"на стройке секция одна, а названо {len(sections)}"
f" ({', '.join(sections)})")
raise Usage(f"стадия уже «{STAGE_RU[want]}» ({want}) — менять нечего")
declaring = lay.stage not in STAGES
lines = read_lines(lay.index("backlog"))
heads = section_headers(lines)
# Тело каждой секции — всё, что под её заголовком: и строки-пункты, и проза.
# Собирается оно в первую новую секцию в прежнем порядке секций: другого
# порядка машина не знает, а выдумать его значило бы переставить чужую
# очередь.
head = lines[:heads[0][0]] if heads else lines
body: list[str] = []
for k, (i, _) in enumerate(heads):
end = heads[k + 1][0] if k + 1 < len(heads) else len(lines)
body += lines[i + 1:end]
moved = sum(1 for line in body if INDEX_ENTRY.match(line))
if not heads:
raise Usage(f"в {lay.name('backlog')} нет ни одной секции — стадию"
f" объявлять не над чем. Заведи секцию заголовком «## …»"
f" и повтори")
current = [name for _, name in heads]
out = [*head]
for k, s in enumerate(sections):
out.append(f"## {s}")
if k == 0:
out += body
if a.sections is not None and declaring:
raise Usage("--sections при объявлении стадии не принимается: объявление"
" называет то, что уже верно, и беклог не переразмечает."
" Секции меняет `move <слаг> --section <секция> --reason …`")
sections = uniq_sections(a.sections) if a.sections is not None else current
if not sections:
raise Usage("пустой список секций")
if want == BUILD and len(sections) > 1:
raise Usage(
f"на стройке беклог — один список от базы к деталям, а секций"
f" {len(sections)}: {', '.join(sections)}. Слить их машина не берётся —"
f" порядок строк в слитом списке знает только человек. Либо слей сам"
f" (`move <слаг> --section <куда> --reason …`) и повтори, либо назови"
f" итоговую секцию явно: `stage {BUILD} --sections <имя>`"
+ (" (при объявлении стадии этот флаг не принимается —"
" сперва слей руками)" if declaring else ""))
merge = sections != current
plan = Plan()
plan.index(lay, "backlog", out)
# Категория в файлах — производна от заголовка индекса, и разъехаться ей
# нельзя: `check` назовёт это дрейфом на первой же записи.
retyped = 0
for task in tasks_of(lay).values():
if task["section_raw"] == sections[0]:
continue
text = meta_updated(task["path"], section=sections[0],
rtype=task["type"] or None)
if text is None:
continue
plan.file(task["path"], text)
retyped += 1
plan.commit()
conf.set_section_key(lay.project, "tasks", STAGE_KEY, want)
skipped: list[str] = []
moved, retyped = 0, 0
was = STAGE_RU.get(lay.stage, "не объявлена")
print(f"стадия: {was}{STAGE_RU[want]}")
print(f" секции беклога: {', '.join(sections)};"
f" перенесено строк {moved}, поправлена «{PLACE_KEY}» у {retyped} файлов")
if want == SUPPORT:
if merge:
# Тело каждой секции — всё, что под её заголовком: и строки-пункты, и
# проза. Собирается оно в первую новую секцию в прежнем порядке секций:
# другого порядка машина не знает, а выдумать его значило бы переставить
# чужую очередь.
head = lines[:heads[0][0]]
body: list[str] = []
for k, (i, _) in enumerate(heads):
end = heads[k + 1][0] if k + 1 < len(heads) else len(lines)
body += lines[i + 1:end]
moved = sum(1 for line in body if INDEX_ENTRY.match(line))
lines = [*head]
for k, s in enumerate(sections):
lines.append(f"## {s}")
if k == 0:
lines += body
# Категория в файлах производна от заголовка индекса, и разъехаться ей
# нельзя: `check` назовёт это дрейфом на первой же записи.
for name, task in tasks_of(lay).items():
if task["section_raw"] == sections[0]:
continue
text = meta_updated(task["path"], section=sections[0],
rtype=task["type"] or None)
if text is None:
skipped.append(name) # мета не пересобирается — скажем вслух
continue
plan.file(task["path"], text)
retyped += 1
# Шапка объявляет стадию, и читают о смысле порядка строк именно её. Не
# переписать её значило бы оставить в индексе прямое враньё.
span = stage_block_span(lines)
if span is not None:
lines[span[0]:span[1]] = stage_block(want)
# Место сырья производно от типа, и слияние секций его нарушает: сырьё из
# второй полки оказывается в середине списка. Без этого шага переход
# оставлял бы каталог красным на ровном месте.
plan.index(lay, "backlog", raw_last(lines, {n for n, t in tasks_of(lay).items()
if raw_research(lay, t)}))
plan.commit()
# Конфиг: файл есть — правим ключ, файла нет — заводим скелетом. Иначе
# объявление стадии в проекте без `.av-dev.toml` рождало бы конфиг без
# версии раскладки, то есть меняло один отказ `check` на другой.
if (lay.project / CONFIG_NAME).is_file():
conf.set_section_key(lay.project, "tasks", STAGE_KEY, want)
said = [f"{CONFIG_NAME}: [tasks] {STAGE_KEY} = «{want}»"]
else:
said = write_config(lay.project, {STAGE_KEY: want})
print(f"стадия: {'объявлена' if declaring else STAGE_RU.get(lay.stage, '') + ''}"
f" {STAGE_RU[want]}")
for line in said:
print(f" {line}")
if merge:
print(f" секции слиты в «{sections[0]}»: перенесено строк {moved},"
f" поправлена «{PLACE_KEY}» у {retyped} файлов")
else:
print(f" секции беклога не тронуты: {', '.join(sections)}")
if span is None:
print(f" шапка {lay.name('backlog')} не размечена ({STAGE_OPEN}) —"
f" абзац про стадию перепиши сам: он называет прежнюю")
for name in skipped:
print(f" НЕ ТРОНУТ {name}: мета не пересобирается (нет поля"
f" **{PLACE_KEY}:**) — «{sections[0]}» проставь сам")
if declaring:
print(" беклог не тронут: объявление называет то, что уже верно")
elif want == SUPPORT:
print(" порядок строк с этого момента значит важность, а не зависимость:"
" прежний план шёл по зависимости, и как очередь он не расставлен."
" Первый заход — груминг (скилл av-dev:task-groom)")
@@ -2931,10 +3124,10 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
# Версия та же, что у `init`: каталог выводится из чужой раскладки сегодня и
# сегодняшним форматом, сколько бы лет ни было тому, из чего он выведен.
# Имён частей здесь нет — адаптация раскладывает всё по умолчаниям.
for path, text in init_files(lay, sections, stage_name, {}).items():
skeleton = init_files(lay, sections, stage_name)
for path, text in skeleton.items():
wr.file(path, text)
backlog_lines = init_files(lay, sections, stage_name,
{})[lay.index("backlog")].splitlines()
backlog_lines = skeleton[lay.index("backlog")].splitlines()
renames: dict[str, str] = {}
for it in pl.get("items", []):
@@ -2968,7 +3161,7 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
entry_line(lay, title, slug, it.get("why", "")))
if pl.get("rejected"):
head = init_files(lay, sections, stage_name, {})[lay.index("rejected")]
head = skeleton[lay.index("rejected")]
body = []
for line in pl["rejected"]:
line = re.sub(r"Был приоритет:", "Была секция:", line)
@@ -2979,8 +3172,14 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
# Через `index`, а не `file`: отбивка секций живёт на записи индекса, и
# каталог, собранный в обход неё, встречал бы человека ошибкой `check`
# на первом же прогоне.
wr.index(lay, "backlog", backlog_lines)
# на первом же прогоне. По той же причине здесь же место сырья: карта могла
# положить разведку без «Вопроса» в середину списка, и `check --fix` потом
# переставил бы её — то есть тронул бы порядок, который человек подтвердил.
raw_now = {f"{it.get('slug') or it['old_slug']}.md" for it in pl.get("items", [])
if (it.get("type") or "").lower() == RESEARCH
and not re.search(rf"^##\s+{re.escape(lay.cfg['question_heading'])}\s*$",
it.get("body", ""), flags=re.M | re.I)}
wr.index(lay, "backlog", raw_last(backlog_lines, raw_now))
if a.dry_run:
print(f"пробный прогон: записалось бы файлов {len(wr.writes)},"
@@ -3081,7 +3280,7 @@ def main() -> int:
p.add_argument("--rm-tag", dest="rm_tag")
p.add_argument("--dir")
p = sub.add_parser("move", help="переставить строку: место в очереди или другая секция")
p = sub.add_parser("move", help="переставить строку: место в списке или другая секция")
p.add_argument("slug")
p.add_argument("--section", help="другая категория беклога;"
" без него — текущая секция записи")
+4 -1
View File
@@ -127,7 +127,10 @@ def vocabularies(root: Path) -> tuple[dict[str, set[str]], dict[str, str]]:
tasks_names = {stem(tasks.DEFAULTS[k]) for k in tasks.PATH_KEYS}
tasks_names |= {stem(tasks.CONFIG_NAME)}
retired = {stem(n): why for n, why in docs.RETIRED.items()}
# Упразднённое собирается у **обоих** владельцев: слоты канона знает
# `docs.py`, части каталога задач — `tasks.py`, и перечень одного из них
# молчал бы про упразднения другого.
retired = {stem(n): why for n, why in {**docs.RETIRED, **tasks.RETIRED}.items()}
return {"docs": docs_names, "tasks": tasks_names}, retired