задачи: починена смена стадии, разобраны находки ревью плагина
Команда 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:
@@ -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",
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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`, разбор |
|
||||
|
||||
<!-- /копия: тема-метка-глубина -->
|
||||
|
||||
Две глубины, которые ты назначаешь:
|
||||
|
||||
- **сверка** — открыть дом, открыть дифф, сравнить. Один-два вопроса на тему,
|
||||
|
||||
@@ -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` / `Границы покрытия`.
|
||||
|
||||
Перед секциями — сводка: размер, сложность и метка с обоснованием разметки и режим прогона,
|
||||
состояние гейта, **план с исходом по каждой теме**, сколько находок пришло на
|
||||
вход и сколько осталось.
|
||||
Перед секциями — сводка: режим прогона, состояние гейта, **план с исходом по
|
||||
каждой теме**, сколько находок пришло на вход и сколько осталось. На прогоне
|
||||
**с меткой** к этому добавляются размер, сложность и метка с обоснованием
|
||||
разметки; на прогоне **без метки** их место занимает строка «прогон без метки,
|
||||
план сценария обслуживания» — выдумывать метку задним числом нельзя, её никто
|
||||
не снимал.
|
||||
|
||||
## Ограничения
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ color: green
|
||||
постановке. Он же путь понизить требования решением, принятым до
|
||||
проектирования.
|
||||
|
||||
|
||||
## Чего ты не проверяешь
|
||||
|
||||
Не своё бывает двух разных родов, и поступают с ними по-разному.
|
||||
|
||||
+14
-2
@@ -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` на
|
||||
|
||||
@@ -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` по записям задач, — и разойтись формой они не должны.
|
||||
|
||||
<!-- дом: вычитка-доклад -->
|
||||
|
||||
|
||||
@@ -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. Объяви переходное состояние
|
||||
|
||||
|
||||
@@ -351,9 +351,10 @@ kebab-case.** Причина не эстетическая: имя файла с
|
||||
скриптом и своими проверками; где каталог лежит и как названы его части, говорит
|
||||
секция `[tasks]` в `.av-dev.toml`. Версия там одна на всю раскладку: канон и
|
||||
каталог задач двигаются вместе, потому что ведёт их один плагин.
|
||||
Канон **резервирует место** в `docs/` и внутрь не смотрит:
|
||||
`docs.py` каталог не открывает, его отсутствия не считает дрейфом и согласованность
|
||||
задач не проверяет. Проект, не заведший каталог задач, их не ведёт
|
||||
Каталог лежит **в корне репозитория, вне `docs/`** — места в `docs/` канон ему
|
||||
больше не резервирует (переезд — запись 11 закрытого журнала), а внутрь не
|
||||
смотрит и подавно: `docs.py` каталог не открывает, его отсутствия не считает
|
||||
дрейфом и согласованность задач не проверяет. Проект, не заведший каталог задач, их не ведёт
|
||||
вовсе, и отказом это быть не может.
|
||||
|
||||
Раскладку, форму записи и команды держит скилл `av-dev:task-track`. Ниже — то,
|
||||
|
||||
@@ -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 отсюда. Записи от
|
||||
этого не переписываются: порядок между ними прежний, добавлено одно условие
|
||||
входа.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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; канон это допускает прямо, и в записи
|
||||
источник называется.
|
||||
|
||||
@@ -195,10 +195,17 @@ flowchart TD
|
||||
накопленные до этого места, и находки ревью с пометкой `развилка`;
|
||||
- **что дальше**, если возражений нет.
|
||||
|
||||
Проверка на «простой язык» одна и механическая: **в тексте нет `SHALL`, нет имён
|
||||
файлов и функций там, где вещь называется по-русски, и нет слов, которых нет в
|
||||
паспорте проекта.** Не проходит — переписывай, а не объясняй, почему иначе
|
||||
нельзя.
|
||||
Проверка на «простой язык» одна и механическая, и она общая у чекпоинтов всех
|
||||
трёх сценариев — поэтому её дом здесь, а у соседей помеченные копии:
|
||||
|
||||
<!-- дом: чекпоинт-простой-язык -->
|
||||
|
||||
**в тексте нет `SHALL`, нет имён файлов и функций там, где вещь называется
|
||||
по-русски, и нет слов, которых нет в паспорте проекта.**
|
||||
|
||||
<!-- /дом: чекпоинт-простой-язык -->
|
||||
|
||||
Не проходит — переписывай, а не объясняй, почему иначе нельзя.
|
||||
|
||||
Длина — экран. Длинное объяснение не читают, а пролистывают, и чекпоинт
|
||||
превращается в ритуал одобрения.
|
||||
|
||||
@@ -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`, трогая один узел. Поэтому в плане
|
||||
стоят три строки, а не одна: размер, сложность и метка — каждая со своим
|
||||
|
||||
@@ -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/` | находка принята и не специфична для одного места | промоут |
|
||||
|
||||
@@ -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` по беклогу показывает,
|
||||
что и когда закрыто, потому что закрытие коммитится отдельным коммитом учёта.
|
||||
|
||||
Известные обходы:
|
||||
|
||||
@@ -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`, и это не отменяет разреза. Оба судят по
|
||||
**записанному правилу** — семь пунктов формы против правил языка, — а их находка
|
||||
**записанному правилу** — шесть пунктов формы против правил языка, — а их находка
|
||||
приходит готовой формулировкой, которую читает и отклоняет человек, а не молча
|
||||
реализует оркестратор. Ошибка здесь стоит строки чтения, и платить за неё верхней
|
||||
моделью не за что.
|
||||
|
||||
@@ -100,9 +100,10 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
|
||||
`apply` печатает состояние по факту: сколько задач не собрало разделы своего типа
|
||||
(для `check` это не ошибка, а строка здоровья, но `ready` такую задачу не
|
||||
пропустит). Закрывается это **порциями груминга** — скилл `groom`, 5–8 задач за
|
||||
порцию: превратить «готово, когда» в критерии с оракулами, вынуть вопросы из
|
||||
прозы в раздел «Вопросы».
|
||||
пропустит). Закрывается это **порциями по 5–8 задач**: превратить «готово,
|
||||
когда» в критерии с оракулами, вынуть вопросы из прозы в раздел «Вопросы». На
|
||||
доработке это груминг (скилл `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 скилла).
|
||||
Место сырья **из него изъято**: оно производно от типа и заполненности, а не от
|
||||
чьего-то решения, и потому его проверяет и чинит машина. Приоритетом оно не
|
||||
становится: сырьё не берут вовсе, и место в конце говорит именно это.
|
||||
|
||||
@@ -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="другая категория беклога;"
|
||||
" без него — текущая секция записи")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user