diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 805b3b3..b2473d1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -19,11 +19,6 @@ "name": "av-dev-git", "source": "./av-dev-git", "description": "Git-обвязка для личных проектов. Скилл commit — сообщения коммитов в личном стиле (русский, scope-префикс, средняя детализация, без co-authored)." - }, - { - "name": "av-dev-backlog", - "source": "./av-dev-backlog", - "description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями. Оставлен до перевода последнего проекта; новые проекты не подключают." } ] } diff --git a/DECISIONS.md b/DECISIONS.md index f78ed66..8872c07 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -373,8 +373,9 @@ openspec/ докладывает исход, записей учёта не трогает. **Q. `av-dev-backlog` помечается устаревшим и остаётся** до перевода jellybit. -Описание переписывается так, чтобы не ловить триггер «добавь задачу в беклог» — -иначе агент выбирает между ним и `av-dev-pm` случайно. +*(заменено на тему 30: плагин удалён раньше этого срока — условие пережило свою +причину.)* Описание переписывается так, чтобы не ловить триггер «добавь задачу в +беклог» — иначе агент выбирает между ним и `av-dev-pm` случайно. ### Что из этого следует @@ -730,7 +731,9 @@ pyrefly: в окружении нет ничего, кроме линтеров, опечатка, и три этих правила давали **311 срабатываний из 338** — шум, в котором тонут остальные 27. -**HH. `av-dev-backlog` исключён из проверки.** Плагин помечен устаревшим и живёт +**HH. `av-dev-backlog` исключён из проверки.** *(исчерпано темой 30: плагин +удалён, исключение снято из `pyproject.toml` и `copies.py`.)* Плагин помечен +устаревшим и живёт до перевода последнего проекта, после чего удаляется целиком. Шесть его находок косметические (`os.replace`, `l` как имя), а правка замороженного кода без тестов — риск без выгоды. Исключение уходит вместе с плагином. @@ -2041,3 +2044,43 @@ dev-skills — **маркетплейс плагинов**: скилл комм 113. **Число в документе — обязанность, которую никто не берёт.** Счётчик тем, коммитов, правок протухает молча; формулировка без числа дешевле его сопровождения. + +## 30. `av-dev-backlog` удалён (2026-08-05) + +Плагин был помечен устаревшим решением Q и жил до перевода jellybit. Удалён +раньше этого срока. + +**ААББВВ. Замороженный плагин стоит дороже, чем кажется.** Он не менялся, но +платил собой в каждой проверке репозитория: `exclude` в `pyproject.toml`, +`SKIP_DIRS` в `copies.py`, два абзаца README, оговорка в описании маркетплейса, +чтобы не ловить триггер «добавь задачу в беклог». Пять исключений ради кода, +который никто не читает, — и каждое надо было объяснять всякий раз, когда +кто-нибудь спрашивал, почему проверка обходит каталог. + +**ААББГГ. Понимание старой раскладки уехало из плагина раньше самого плагина.** +`docs/backlog/` читает не `backlog.py`, а `av-dev-pm:tasks` — `adopt.md` и +адаптер в `tasks.py` держат ту же раскладку как **вход миграции**. Плагин +перестал быть единственным, кто её знает, ещё когда писался `adopt`; условие +«живёт до перевода последнего проекта» с тех пор охраняло пустоту. + +**ААББДД. Опасение про порядок снятия не подтвердилось.** Удаление опередило +снятие: на jellybit плагин оставался включённым, когда записи в маркетплейсе уже +не было, и ожидалась ручная чистка `enabledPlugins` и `installed_plugins.json`. +`claude plugin uninstall` отработал штатно — он идёт **по реестру, а не по +манифесту маркетплейса**, и отсутствие записи там ему безразлично. Предупреждение +из README снято, вместо него записан проверенный факт. + +### Что из этого следует + +114. **Устаревшее удаляют, а не замораживают.** Заморозка выглядит бесплатной, + но растекается исключениями по конфигам и требует объяснения в каждом + месте, куда попала. Если удалять пока рано — назвать условие и срок; условие + без срока переживает свою причину. +115. **Условие «живёт до X» проверяют на живость, а не на X.** Здесь X (перевод + jellybit) не наступил, но причина условия отпала раньше: знание раскладки + переехало в `adopt`. Перепроверять надо основание, иначе условие держит само + себя. +116. **Порядок снятия и удаления из маркетплейса свободный.** `uninstall` живёт + реестром, манифест ему не нужен. Правило записано после проверки, а не + из осторожности, — и осторожность здесь стоила бы лишнего абзаца в README + про починку, которой не бывает. diff --git a/README.md b/README.md index 252a772..f539649 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,6 @@ архитектура, обязательный триаж. Девять агентов-проходов, четыре ступени стоимости: `quick`, `standard`, `wide`, `deep`. - **av-dev-git** — `commit`: сообщения в личном стиле. -- **av-dev-backlog** — **устарел**, заменён `av-dev-pm`. Живёт до перевода - последнего проекта; как снять с проекта — [Снятие](#снятие). Соглашение об именах: имя **плагина** длинное с префиксом `av-dev-`, имена **скилов** внутри — короткие. Вызов выходит вида `/av-dev-<плагин>:<скилл>`. @@ -183,25 +181,25 @@ EOF ## Снятие -Действие, обратное подключению. Актуально для `av-dev-backlog`: плагин устарел, -и с каждого проекта снимается по мере перевода задач на канон `docs/tasks/`. - -**Сначала перевод, потом снятие.** Задачи переводит `/av-dev-pm:canon` -(`docs/backlog/` → `docs/tasks/`). Снять плагин раньше — остаться со старой -раскладкой и без скилла, который её понимает. +Действие, обратное подключению. ```bash cd /path/to/project -claude plugin uninstall av-dev-backlog@av-dev-skills --scope project +claude plugin uninstall <плагин>@av-dev-skills --scope project ``` Команда правит два места: убирает строку из `enabledPlugins` в `.claude/settings.json` проекта и запись из реестра `~/.claude/plugins/installed_plugins.json`. Снимок в -`~/.claude/plugins/cache/av-dev-skills/av-dev-backlog/<версия>/` не трогает — он +`~/.claude/plugins/cache/av-dev-skills/<плагин>/<версия>/` не трогает — он общий для всех проектов. Маркетплейс тоже остаётся: `extraKnownMarketplaces` нужен остальным плагинам. +**Удалять из маркетплейса можно и до снятия с проектов.** `uninstall` идёт по +реестру, а не по `marketplace.json`, и снимает плагин, записи о котором в +манифесте уже нет. Проверено на `av-dev-backlog`: удалён из маркетплейса, +снят с jellybit после — команда отработала штатно. + `--scope project` обязателен по той же причине, что и при установке: умолчание у команды — user. `cd` в проект обязателен, но здесь ошибка слышна — вызванная не оттуда, команда откажется словами `is not installed in project scope`, а не @@ -246,10 +244,6 @@ uv run pyrefly check # типы линтеров, и любой сторонний импорт у него не разрешается. Список запретов — не перечень мира, настоящий страж второй. -`av-dev-backlog` из проверки исключён намеренно: плагин помечен устаревшим и -живёт до перевода последнего проекта, после чего удаляется целиком. Правки в -замороженный код — риск без выгоды. - ## Проверка фронтматтеров Фронтматтер читает не человек, а загрузчик: по `name` он разрешает вызов, по diff --git a/REMAINING.md b/REMAINING.md index 1050383..7f58b3c 100644 --- a/REMAINING.md +++ b/REMAINING.md @@ -73,10 +73,6 @@ check` сверяет версию, но не то, что миграционн касался, и пересмотр, отменяющий решение из документа, к которому не притрагивались, он не увидит. Механической проверки по-прежнему нет. -**Что делать с `av-dev-backlog` после перевода jellybit.** Помечен устаревшим и -переписан так, чтобы не ловить триггер. Удалять его из маркетплейса или оставить -как есть — решится, когда jellybit переедет. - ## Известные пределы — приняты, чинить не планируется **Транзакций на несколько файлов нет.** POSIX её не даёт без журнала. Окно сжато diff --git a/TODO.md b/TODO.md index 084e4ef..33633ea 100644 --- a/TODO.md +++ b/TODO.md @@ -155,7 +155,7 @@ `research` (сырьё: тип есть, «Вопрос» пуст), logical-title-model → ADR (H) - [ ] `docs/backlog/` → `docs/tasks/` - [ ] удалить проектные копии скиллов и агентов (4 из REMAINING) -- [ ] `av-dev-backlog` удалить из маркетплейса +- [x] `av-dev-backlog` удалить из маркетплейса и снять с проекта (тема 30) ## 6. Канон версии 3 — повысить живые проекты (тема 17) diff --git a/av-dev-backlog/.claude-plugin/plugin.json b/av-dev-backlog/.claude-plugin/plugin.json deleted file mode 100644 index 36081b5..0000000 --- a/av-dev-backlog/.claude-plugin/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "av-dev-backlog", - "description": "УСТАРЕЛ, заменён плагином av-dev-pm. Старый формат беклога: один каталог задач с индексом README и приоритетами секциями, без целей и спринтов. Оставлен до перевода последнего проекта, который на нём ещё живёт; новые проекты не подключают.", - "author": { - "name": "Anton Vakhrushev", - "email": "anwinged@gmail.com" - } -} diff --git a/av-dev-backlog/skills/backlog/SKILL.md b/av-dev-backlog/skills/backlog/SKILL.md deleted file mode 100644 index 67a59df..0000000 --- a/av-dev-backlog/skills/backlog/SKILL.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -name: backlog -description: "УСТАРЕЛ — используй скилл av-dev-pm:tasks. Старый формат беклога (один каталог задач, индекс README, приоритеты секциями, без целей и спринтов). Вызывать ТОЛЬКО в проекте, который на этот формат ещё не переведён, и только если прямо названо имя backlog. Во всех остальных случаях, включая любую просьбу завести задачу, идею или разобрать находки ревью, работает av-dev-pm:tasks." ---- - -> **Этот скилл устарел.** Формат заменён каноном `docs/tasks/` из плагина -> `av-dev-pm` (цели вместо приоритетов, спринт с заморозкой набора, `REJECTED.md` -> с причинами). Перевод проекта делает скилл `av-dev-pm:canon`. Скилл оставлен до -> перевода последнего проекта, который на нём ещё живёт, и будет удалён. - - -# Беклог - -Беклог — каталог markdown-файлов: одна задача = один файл `.md`, плюс -строка в индексе `README.md`. Скилл ведёт беклог: заводит, чистит, приоритизирует, -дробит, штурмует идеи. **Реализацией не занимается** — это дело пайплайна задачи. - -## Три правила, из которых всё следует - -Ситуация не покрыта инструкцией — решай по ним. - -1. **Беклог гниёт с той стороны, где его пополняют.** Заведение — самая частая - операция и с худшим отказом: из одного разговора рождается пять файлов, и - груминг потом разгребает то, чего не надо было заводить. Дедупликация и фильтр - на входе дешевле любой чистки. Заводим только то, что **не делаем сейчас** и о - потере чего пожалеем. -2. **Файл — источник истины, индекс производен.** Разошлись — неправ индекс. - Согласованность механизируема и проверяется командой, а не вниманием: всё, что - ловит `backlog.py check`, не должно попадать ни в чек-лист, ни в промпт. -3. **Причина переживает запись.** Приоритет без причины будет переспорен на - следующем груминге; выкинутая без причины задача вернётся через квартал тем же - текстом. Реализованная задача оставляет след в коммите и спеке — выкинутая не - оставляет ничего, поэтому у неё есть кладбище. - -## Инструмент (`backlog.py`) - -Пусть `bl="$CLAUDE_PLUGIN_ROOT/skills/backlog/scripts/backlog.py"`. - -``` -python3 $bl check # согласованность + метрики здоровья, exit 1 при расхождениях -python3 $bl check --fix # + починить безопасный дрейф (секция, заголовок, дубли) -python3 $bl list --stale # от самой залежавшейся; ещё --priority --type --tag -python3 $bl add --slug S --title T --priority P [--type idea|epic] [--hook H] [--reason R] [--tag a,b] -python3 $bl edit S [--title T] [--hook H] [--type idea|epic|task] # переименовать / сменить хук, тип -python3 $bl move S --priority P [--reason R] # перенести в другую секцию -python3 $bl close S --reason R # на кладбище + удалить (выкинута) -python3 $bl close S --implemented # просто удалить (реализована, есть коммит) -python3 $bl init [--sections "..."] # завести беклог в новом проекте -``` - -Тип задачи — английское ключевое слово `idea` / `epic` / `task` (как и прочие -токены команд); `task` префикса не несёт, `idea`/`epic` кодируются `[idea]`/ -`[epic]` в заголовке. Текст задачи при этом русский. - -**Мутации правят файл и индекс заодно** — руками строку индекса или мета-строку -не пиши, зови `add`/`edit`/`move`/`close`. Смена заголовка, хука или типа (в том -числе понижение задачи до `[idea]`) — это `edit`, а не ручная правка H1 и -индекса: `edit` держит их в синхроне. Механика (слаг в имени, секция по -приоритету, формат кладбища, экранирование ввода) не может рассогласоваться, -потому что её делает скрипт. Тело задачи скрипт не трогает — `add` кладёт -заголовок, мета-строку и плейсхолдер, а контекст, шаги и ссылки ты дописываешь -редактором (пока плейсхолдер на месте, `check` напоминает, что тело не дописано). - -`check` — единственный судья согласованности; что именно он ловит, скажет его -вывод, здесь не пересказываем. Гоняй его **в начале сессии** и **после каждой -правки**, даже если правил мутациями: дрейф мог накопиться раньше. Накопившийся -дрейф чини `check --fix` — он детерминированно правит безопасное (секция по файлу, -заголовок из H1, дубли строк), а неоднозначное (ссылка на исчезнувший файл, -битые строки) выносит тебе. Это идёт строкой доклада. - -Формат файла, мета-строки, слага, индекса и кладбища — -[references/task-format.md](references/task-format.md). Там же тест «готова к -взятию». - -## Сценарии - -### Завести задачу или идею из диалога - -1. **Фильтр.** Делаем прямо сейчас — не заводим. Не пожалеем о потере — не - заводим. Родилось три кандидата — покажи их и спроси, какие заводить: молча - заведённая пачка и есть тот самый отказ из правила 1. -2. **Дедуп.** `list` плюс поиск по слагам, хукам и телам (`grep -ril`), **включая - `CLOSED.md`**. Нашлось в беклоге — **дописываем в существующий файл**, а не - заводим соседний. Нашлось на кладбище — покажи пользователю ту строку и что - изменилось с момента отказа: та же идея вернулась через диалог, а не через - ревью. Две задачи об одном — самая дорогая находка груминга. -3. **Тип по тесту готовности** (см. task-format): проходит — задача (`--type task`, - без префикса), не проходит — идея (`--type idea`), проходит по пользе, но не - делается одним заходом — эпик (`--type epic`, сперва декомпозиция). -4. `add --slug … --title … --priority … --hook …` (тип, причину, теги — по - месту). Хук отвечает «почему это в беклоге», а не пересказывает первый абзац. - Затем допиши тело файла. -5. `check`. - -### Разобрать находки аудита или ревью - -Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности — тоже -источник задач, но с зеркальной диалогу опасностью: не пять файлов из одной -мысли, а сорок файлов из сорока сырых находок. Защита та же, что в самом ревью: -кластеризация по причине, дедуп против беклога, находка без свидетельства → идея, -а не задача, и карта кластеров пользователю до создания файлов. Порядок и -отображение серьёзности — [references/from-review.md](references/from-review.md). - -### Груминг - -Интерактивная сессия порциями, по дате правки из git и с правилом остановки — -[references/grooming.md](references/grooming.md). Ключевое: перед вопросом -пользователю проверь по коду и спекам, не сделано ли уже попутно, — это самая -частая находка и она не требует ничьего решения. - -### Приоритизация - -Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку. -Никаких очков и часов: уровни те, что есть в секциях индекса. - -- Меняешь уровень — `move --priority <новый> --reason <причина>`; причина - уезжает в мета-строку. -- Повышаешь — назови, **что именно эта задача обгоняет**. Повышение без - проигравшего это не приоритизация, а согласие с последним, кто говорил. -- Задача, давно лежащая в нижней секции и не двигавшаяся (по дате git), — - кандидат на кладбище, а не на новый круг «оставить как есть». - -### Декомпозиция и штурм идеи - -[references/split.md](references/split.md). Обе операции превращают одну запись в -несколько, и у обеих есть проверяемый тест: части должны **мерджиться порознь** и -**каждая давать видимую пользу**, а у штурма исход «выкинуть» — полноправный. - -## Общее для всех сценариев - -- **Кладбище.** Задача уходит из беклога без реализации → `close --reason - <причина>`: скрипт пишет строку в `CLOSED.md` (дата, слаг, заголовок, причина, - бывший приоритет) и удаляет файл со строкой индекса. Реализованные туда не идут - — у них есть коммит, спека и ADR; для них `close --implemented`. -- **Границы покрытия в отчёте.** Любая сессия груминга, приоритизации или штурма - заканчивается строкой «просмотрено N из M, не трогали — …». Отчёт без неё - сообщает «беклог разобран», не сообщая, какая его часть осталась нетронутой. -- **Развилки — пользователю.** Через `AskUserQuestion`, с уже сформулированным - предварительным суждением (рекомендация — первым вариантом). Что выкинуть, что - повысить, какая рамка идеи верна — решение пользователя. Слаг, формулировка, - порядок строк в индексе — механика, делаем сами. -- **Не больше трёх вопросов за раз.** Пачка длиннее трёх тяжела для ответа; - решений больше — веди **несколько итераций** диалога по ≤3, а не один - перегруженный запрос. Между итерациями применяй уже решённое. -- **Ничего не удаляем молча.** Файл задачи исчезает только через `close` — - `--reason` (выкинута) или `--implemented` (реализована). Прямого `rm` нет. - -## Переносимость - -Скилл независим от **языка программирования, сборки, CI и трекера**: он ничего не -знает ни про Go, ни про npm, ни про конкретный багтрекер — беклог для него просто -каталог markdown. Текст задач — русский (язык документации проекта); зашита только -латиница слага. - -- **Каталог беклога**: аргумент → указатель в `CLAUDE.md` проекта → поиск - (`docs/backlog`, `backlog`, `doc/backlog`, `docs/tasks`). Не нашёлся — это новый - проект: `init` заводит индекс и кладбище (секции по умолчанию высокий/средний/ - низкий, `--sections` переопределяет). -- **Слаг** — латиница kebab-case всегда; заголовок, тело, хук — по-русски. -- **Уровни приоритета** берутся из заголовков секций индекса как есть, их - количество и названия — дело проекта. -- **Имена служебных файлов** (`README.md` — индекс, `CLOSED.md` — кладбище) - фиксированы скиллом, не проектом. - -Проектные тонкости (куда переезжает суть реализованной задачи, кто удаляет файл, -как беклог связан с трекером-инбоксом) описаны в `CLAUDE.md` проекта — прочитай -его перед работой. - -## Чего этот скилл не делает - -Не пишет код, не заводит спеки и change, не берёт задачу в работу — этим -занимается пайплайн задачи проекта, а этот скилл владеет только форматом и -содержимым беклога. Не решает за пользователя, что важно. Не переоформляет -существующие задачи «заодно»: правится то, чего касается операция. diff --git a/av-dev-backlog/skills/backlog/references/from-review.md b/av-dev-backlog/skills/backlog/references/from-review.md deleted file mode 100644 index 8bc0f22..0000000 --- a/av-dev-backlog/skills/backlog/references/from-review.md +++ /dev/null @@ -1,84 +0,0 @@ -# Задачи из аудита и ревью - -Ревью и аудиты — код-ревью, архитектурный проход, аудит безопасности, любой -разбор другим агентом — порождают находки, часть которых становится задачами -беклога. Это отдельный интейк со своей опасностью, **зеркальной** интейку из -диалога. - -- Интейк из диалога грешит переполнением: из одной мысли рождается пять файлов. -- Интейк из ревью грешит сваливанием: сорок сырых находок превращаются в сорок - файлов. Беклог раздувается, а следующий груминг склеивает их обратно. - -Защита от сваливания — та же, что в самом ревью: **кластеризация по причине, а не -файл-на-находку.** Если у ревью был триаж — половина работы уже сделана, бери его -выход. Если нет — триажируй сам, прежде чем заводить. - -## Находка агента — не задача - -Мнение агента — **гипотеза, пока у неё нет свидетельства** (падающий тест, -воспроизводимый шаг, положение гайда). Согласие нескольких находок само по себе -достоверность не повышает: это один источник, высказавшийся несколько раз. - -Отсюда фильтр входа, поверх обычного «не делаем сейчас + пожалеем о потере»: - -- **Находка со свидетельством**, отложенная к исполнению → **задача**. - Свидетельство и последствие переносим в тело — это её «почему», то самое, что - переживает запись. -- **Находка без свидетельства / низкой уверенности** → **идея** (`[idea]`), а не - задача. Она не заработала приоритизацию: сравнивать неподтверждённое не с чем. - Её судьба — штурм, где либо найдётся подтверждение, либо она уедет на кладбище. -- **Уже починено по ходу ревью** → **ничего**. Починенное не заводим. -- **Развилка, решённая при ревью** → ничего; решённая «потом» → задача с - зафиксированным вопросом. - -## Порядок - -1. **Возьми выход триажа, а не сырые находки.** Сырой отчёт — это симптомы до - дедупликации; в нём одна причина размазана по нескольким строкам. -2. **Кластеризуй по причине.** Пять находок об одном отсутствующем инварианте — - одна задача, а не пять. Класс мелочи (nits, косметика) — **один пакетный файл** - со списком пунктов, а не файл на каждую запятую. -3. **Дедуп против беклога и кладбища.** Аудит переоткрывает уже заведённое и уже - выкинутое. Нашлось в беклоге — дописываем находку в существующий файл. Нашлось - на кладбище — это сигнал: причина отказа могла устареть, выноси пользователю, а - не заводи молча заново. -4. **Покажи карту до создания файлов.** Кластер → задача / идея / строка в - пакетный файл / уже в беклоге / отброшено — пачкой через `AskUserQuestion`. - Это тот же барьер, что и «три кандидата» в интейке из диалога: массовое - заведение файлов без подтверждения — ровно тот отказ, ради которого интейк из - ревью и выделен. Дешёвая мелочь по явному согласию может заводиться и без - поштучного вопроса — но карта пользователю всё равно предъявляется. -5. **Заводи утверждённое** через `backlog.py add`, с двумя добавками: - - **тег партии** — `add … --tag review-ГГГГ-ММ-ДД` (или `audit-<тема>`), чтобы - весь заход груминга поднимался одной командой `backlog.py list --tag …`; - - **провенанс в теле** — кто нашёл, каким проходом, с каким свидетельством. Без - него через месяц не отличить проверенную находку от догадки. -6. `backlog.py check`. - -## Отображение серьёзности на приоритет - -Правило концептуальное, от полей конкретного отчёта не зависит: - -- **выше серьёзность → выше приоритет.** Самый тяжёлый класс находок → верхняя - секция индекса, следующий → следующая. Отображать словарь серьёзности отчёта на - словарь приоритетов проекта точно нечем — при сомнении спрашивай пользователя. -- **низкая уверенность или нет свидетельства → идея**, не задача. -- **мелочь → строка в пакетный файл**, не отдельный. -- **уже починено / развилка решена сейчас → ничего.** - -Если у ревью структурированный отчёт с полями серьёзности, уверенности, -свидетельства и предписанного действия (например, конвейер ревью jellybit даёт -`Severity`/`Confidence`/`Оракул`/`Действие: инлайн|развилка`) — правило выше -ложится на эти поля механически. Но это пример одного формата, а не требование к -источнику: тот же фильтр применяется к находкам в свободной форме. - -Границы покрытия отчёта — то, что ревью проверить **не смогло**, — не находки и в -задачи не идут: у них нет предмета. Их место — в докладе, не в беклоге. - -## Доклад - -- Источник (какое ревью/аудит, сколько находок на входе). -- Свёрнуто в задачи: N кластеров из M находок, со слагами и тегом партии. -- Что не заведено и почему: починено инлайн, уже в беклоге, ушло в идеи, на - кладбище. -- `backlog.py check`. diff --git a/av-dev-backlog/skills/backlog/references/grooming.md b/av-dev-backlog/skills/backlog/references/grooming.md deleted file mode 100644 index f4b1df0..0000000 --- a/av-dev-backlog/skills/backlog/references/grooming.md +++ /dev/null @@ -1,101 +0,0 @@ -# Груминг беклога - -Цель — выкинуть то, что перестало быть задачей, и вернуть остальному честное -состояние. Не «пересмотреть всё», а «пересмотреть порцию до конца». - -## Порция и правило остановки - -Тридцать задач за один заход — это усталость и штамповка: последние десять -получат «оставить» не потому, что живы, а потому, что сессия затянулась. - -- **5–8 задач за сессию.** Больше — только если пользователь настаивает, и тогда - разбей на явные порции с промежуточным докладом. -- **Отбор порции** — один из: - - `backlog.py list --stale` — самые залежавшиеся по дате последней правки в - git; поле «дата касания» заводить не надо, git её уже хранит; - - одна секция приоритета целиком; - - один тег (`--tag`) — например, задачи, пришедшие из одного ревью; - - список от пользователя. -- **Останавливайся на границе порции**, даже если «ещё чуть-чуть осталось». - -## Что делать с каждой задачей - -Сперва то, что не требует ничьего решения: - -1. **Проверь, не сделано ли уже.** Задача, реализованная попутно в соседнем - изменении, — самая частая находка груминга. Смотри код, спеки, историю - коммитов по ключевым словам задачи. Удаление задачи «как реализованной» — - деструктивно и без следа (кладбище для реализованных не пишется), поэтому - порог улики жёсткий: удаляем (`close --implemented`), только имея - **конкретный коммит или строку спеки**, закрывающие задачу, и ссылка на них - идёт в доклад. Есть лишь косвенные признаки — не удаляй сам, вынеси в пачку - вопросов. Сделана частично → задача сжимается до остатка: тело правишь - редактором, заголовок и хук — через `edit --title … --hook …`. -2. **Проверь, не отменена ли решением.** ADR, спека или архивный change мог - закрыть вопрос иначе. Тогда `close --reason "<ссылка на решение>"`. -3. **Проверь пересечения внутри порции.** Две задачи об одном — содержимое в - одну, вторую `close --reason "слита с <другой-slug>"`. - -Затем — то, что решает пользователь: - -4. **Жива ли она вообще.** Контекст мог измениться: ушла зависимость, отпал - сценарий, обошли иначе. Здесь и звучит вопрос о выкидывании. -5. **Тот ли приоритет** (тест и правила — в SKILL.md и task-format.md). -6. **Задача ли это по-прежнему.** Не проходит тест «готова к взятию» → `edit - --type idea`, и её дальнейшая судьба — штурм, а не приоритизация. - Разрослась → `edit --type epic`, дальше декомпозиция. - -## Храповик - -Сильно залежавшаяся задача — сигнал сама по себе: её либо ни разу не собирались -делать, либо нечем взять. Измеряй наблюдаемым — датой последней правки из git -(`backlog.py list --stale` ставит такие первыми); счётчик «сколько грумингов -пережила» нигде не хранится, поэтому на него не опирайся. - -Задача из верхних строк `--stale`, которую и этот заход оставляет без изменений, -**либо двигается (вверх или на кладбище), либо остаётся с явно записанной -причиной**, почему её держим (`move --priority <тот же> --reason …`). -Молчаливое «оставить как есть» на давно неподвижной задаче — это решение не -принимать решение; запись причины превращает его в осознанное и не даёт тому же -вопросу всплыть на следующем груминге. В примере ниже вариант «оставить» именно -такой — с названной причиной, а не по умолчанию. - -## Интерактив - -- Вопросы — через `AskUserQuestion`, **не больше трёх за раз**. Порция в 5–8 - задач обычно даёт больше трёх суждений — тогда веди несколько итераций по ≤3, а - не по одному на задачу и не одним перегруженным запросом. -- К каждому варианту — **предварительное суждение**, рекомендация первым - вариантом: «предлагаю выкинуть, потому что …». Пользователю дешевле возразить, - чем судить с нуля. -- Всё, что решается фактом (сделано / отменено / дублируется), решай сам и - показывай списком в докладе, а не выноси в вопросы. - -Пример одной итерации — три залежавшихся задачи, механику по ним уже разобрали: - -> **Груминг: 3 залежавшихся (порция по `--stale`)** -> -> 1. `versii-kachestvo-repaki` — репаки, апгрейд 1080p→2160p -> - Выкинуть на кладбище *(рекомендую)* — помечена «не боль», за полгода ни разу не возникла -> - Оставить в низком -> - Поднять в средний -> 2. `backup-sqlite` — бэкап SQLite -> - Оставить в среднем *(рекомендую)* — не сработала, но риск реальный -> - Поднять в высокий — обгоняет `retention-ochistka-bd`: без бэкапа ретеншн опасен -> - Выкинуть -> 3. `guessit-sputnik` — guessit как сервис-спутник -> - Понизить до `[idea]` *(рекомендую)* — не проходит тест «готова к взятию» -> - Оставить задачей в низком - -Каждый вариант несёт причину — ту самую, что уедет в `move --reason` или -`close --reason`. Ответы применяй сразу и, если в порции осталось ещё, следующей -итерацией показывай следующие ≤3. - -## Доклад - -- Что просмотрено: N из M, по какому признаку отобрана порция. -- Изменения списком: удалено (реализовано), на кладбище (с причинами), понижено - до идей, слито, переприоритизировано. -- **Границы покрытия**: сколько задач не трогали и какие именно секции или теги - остались — иначе доклад читается как «беклог разобран». -- `backlog.py check` после правок; результат — строкой в докладе. diff --git a/av-dev-backlog/skills/backlog/references/split.md b/av-dev-backlog/skills/backlog/references/split.md deleted file mode 100644 index 6ed4785..0000000 --- a/av-dev-backlog/skills/backlog/references/split.md +++ /dev/null @@ -1,62 +0,0 @@ -# Декомпозиция и мозговой штурм - -Обе операции превращают одну запись беклога в несколько (или в ноль). Разница в -входе: декомпозиция дробит **готовую задачу**, штурм прорабатывает **идею**, -которая ещё не задача. - -## Тест декомпозиции - -Задачу можно дробить, только если части удовлетворяют **обоим** условиям: - -1. **Мерджатся независимо.** Часть Б не требует, чтобы часть А была уже влита. - Есть порядок «сперва А, потом Б, иначе не собрать» → это не декомпозиция, а - план реализации: шаги остаются **внутри одного файла** в разделе «Шаги». -2. **Каждая даёт видимую пользу.** Часть, полезная только в комплекте с другой, — - не самостоятельная задача. Пользу проверяй тем же тестом «готова к взятию» - (task-format): что станет наблюдаемо иначе именно от этой части. - -Не проходит хотя бы одно — **не дроби**. Ложная декомпозиция плодит файлы, -которые нельзя взять поодиночке, и груминг потом их склеивает обратно. - -## Что делать с родителем - -После разделения родитель **не остаётся** третьей висящей строкой: - -- части полностью замещают его → `close --reason "разложена на a, b"`. - Кладбище здесь — не «выкинули», а именно тот след, что переживает запись: - через квартал вопрос «куда делась задача X» отвечается строкой кладбища со - ссылками на наследников, а не археологией git; -- родитель осмыслен как зонтик → `edit --type epic`, тело — ссылки на - задачи-части, своих шагов у него нет. - -Одно и то же не должно лежать и в родителе, и в части. Задвоение — то же -расхождение, что ловит `check`, только внутри тел. - -## Мозговой штурм идеи - -Идея (`[idea]`) не проходит тест «готова к взятию»: неясно, что именно делаем. -Штурм проясняет — и это **generative-операция, а не applicative**. - -Applicative-штурм («перечисли задачи, следующие из идеи») выдаёт очевидное: -перечисляется то, что уже видно в формулировке. Ценное — на уровень выше. - -1. **Сперва — формы, а не задачи.** Предложи **три разные постановки** идеи и - назови **компромисс каждой**: что она даёт, чем платит, что оставляет за - бортом. Если получилась одна постановка — штурм не состоялся, это applicative. -2. **Вынеси формы пользователю** через `AskUserQuestion` с компромиссами. Рамку - выбирает он: это продуктовое решение, не механика. -3. **Только выбранную форму** дроби по тесту декомпозиции выше. - -**«Выкинуть» — полноправный исход штурма, а не его неудача.** Проработка, честно -показавшая, что пользы нет или она несоразмерна цене, — это результат: идея -уезжает на кладбище с этой самой причиной, и та причина гасит её повторное -появление. - -## Доклад - -- Идея/задача на входе, выбранная рамка (для штурма), задачи-наследники со - слагами и приоритетами. -- Судьба родителя: удалён / стал эпиком / выкинут. -- `backlog.py check` после правок. -- Границы покрытия: какие постановки рассмотрены и какие сознательно отброшены — - чтобы штурм не пришлось повторять с нуля. diff --git a/av-dev-backlog/skills/backlog/references/task-format.md b/av-dev-backlog/skills/backlog/references/task-format.md deleted file mode 100644 index bcd13ab..0000000 --- a/av-dev-backlog/skills/backlog/references/task-format.md +++ /dev/null @@ -1,104 +0,0 @@ -# Формат беклога - -Заголовок, мета-строку и строку индекса ставит `backlog.py add` — руками их не -пишут. Этот файл описывает, что именно скрипт создаёт и что проверяет `check`; -тело задачи (контекст, шаги, ссылки) дописывает агент. - -## Файл задачи - -`.md` в каталоге беклога: - -```markdown -# Раздачи с докачиванием (merge при повторном добавлении) - -**Приоритет:** высокий — блокирует типовой сценарий свежих сериалов · **Теги:** layout, ingest - -Свежий сериал раздают по мере выхода: торрент с 5 из 10 эпизодов позже -перезаливают целиком, пользователь добавляет раздачу повторно. … - -Шаги: -- в плане раскладки отличать «путь занят живой ссылкой того же матча» от коллизии -- merge-раскладка: существующее пропустить, недостающее доложить - -Зависит от правила сходимости. Связано: drafts/logical-title-model.md §6.2. -``` - -- **Заголовок H1** — он же заголовок строки в индексе, дословно. Тип кодируется - префиксом `[idea]` / `[epic]`; обычная задача без префикса. Отдельного поля - типа **нет**: два места для одного факта разъезжаются, а префикс виден прямо в - индексе, где и принимается решение «брать или не брать». -- **Мета-строка** — первая непустая строка после заголовка. Обязателен приоритет, - причина после тире желательна, теги опциональны. Поля разделяются ` · `, их - порядок свободный. `·` — служебный разделитель: в тексте причины его быть не - должно, иначе причина обрежется по нему. -- **Тело** — контекст (почему это вообще задача), принятые решения, шаги, - ссылки на спеки, ADR, черновики, прошлые ревью. Пишется на языке документации - проекта. - -Тело — не план реализации и не спецификация: принятое и реализованное переезжает -в документацию проекта, а файл задачи удаляется. - -## Слаг - -Латиница и цифры, kebab-case, без ведущих, хвостовых и двойных дефисов -(`foo-bar`, не `-foo`, `a--b`). Именуется **по сути задачи, а не по текущей -формулировке**: заголовок будет переписан на груминге, а слаг стоит в ссылках из -других задач, коммитов и черновиков. Транслит русского названия допустим, если -суть иначе не выражается коротко. - -## Индекс - -`README.md` в том же каталоге: преамбула, затем секции по приоритетам, в каждой — -строки вида - -```markdown -- [Заголовок задачи дословно](slug.md) — хук -``` - -Хук отвечает на «почему это лежит в беклоге» одним предложением: состояние, -остаток, боль. Пересказ первого абзаца бесполезен — он уже есть по ссылке. - -Порядок секций задаёт порядок приоритетов, их названия — единственный словарь -уровней. Внутри секции порядок значения не имеет. Секции приоритетов — **единственные -заголовки `##` в индексе**: любой другой `##` в преамбуле проверка сочтёт уровнем -приоритета. - -Индекс **производен**: расходится с файлом — правим индекс. Строку индекса руками -не пишут — её ставит `backlog.py add` в секцию приоритета и двигает `move`. - -## Кладбище — `CLOSED.md` - -Туда уходит задача, покинувшая беклог **без реализации**. Строку пишет -`backlog.py close --reason`, а `check` следит за её форматом: - -```markdown -- 2026-07-23 `versii-kachestvo-repaki` — Версии/качество одного тайтла (репаки, - апгрейд 1080p → 2160p). Причина: калибровка болей — не боль, ни разу не - возникло за полгода. Был приоритет: низкий. -``` - -Реализованные сюда не попадают: у них остаётся коммит, спека, ADR. У выкинутой не -остаётся ничего — и через квартал она возвращается тем же текстом через инбокс. -Кладбище — первое место, куда смотрит дедупликация при заведении. - -Запись на кладбище не запрещает завести задачу заново: изменился контекст — -заводим и ссылаемся на строку кладбища, объясняя, что изменилось. - -## Тест «готова к взятию» - -Задача готова, если из файла отвечаются три вопроса: - -1. **Что станет наблюдаемо иначе**, когда она сделана — снаружи: пользователю, - владельцу сервиса или разработчику. «Отрефакторить X» — не ответ; «перестанет - ломаться Y при Z» — ответ. -2. **По чему видно, что закончено.** Признак завершённости, а не список работ. -3. **Почему приоритет такой** — одна строка. - -Не отвечается первый или второй вопрос → это **идея**, её место в штурме, а не в -приоритизации. Приоритизировать идеи бессмысленно: сравнивается неизвестно что. - -Отвечается всё, но задача не делается одним заходом и не мерджится целиком → -**эпик**, сперва декомпозиция. - -Тест применяется при заведении и на груминге. К старым задачам, которых операция -не касается, задним числом не применяется — беклог не переоформляют «заодно». diff --git a/av-dev-backlog/skills/backlog/scripts/backlog.py b/av-dev-backlog/skills/backlog/scripts/backlog.py deleted file mode 100755 index 1c04547..0000000 --- a/av-dev-backlog/skills/backlog/scripts/backlog.py +++ /dev/null @@ -1,706 +0,0 @@ -#!/usr/bin/env python3 -"""Детерминированный инструмент беклога: файлы задач против индекса README. - -Согласованность беклога — механизируемая вещь, и держать её вниманием агента -дорого и ненадёжно. Скрипт не только проверяет, но и **пишет**: создание, -переименование, перенос между приоритетами и закрытие правят файл и индекс -заодно, так что рассогласовать их вручную нельзя. Всё, что здесь механизировано, -не должно попадать ни в промпт, ни в чек-лист человека. - -Источник истины — файл задачи. Индекс производен от файлов: расходятся — -неправ индекс. - -Тип задачи — ключевое слово (idea | epic | task); по-английски, как и прочие -токены команд. Обычная задача (task) префикса не несёт, idea/epic кодируются -префиксом `[idea]`/`[epic]` в заголовке. Текст самой задачи — русский. - -Использование: - backlog.py check [--dir DIR] [--fix] согласованность (+ здоровье беклога); - --fix чинит безопасный дрейф - backlog.py list [--dir DIR] [фильтры] список задач - --stale от самой залежавшейся (дата последней правки из git) - --priority СЛОВО / --type idea|epic / --tag СЛОВО фильтры - backlog.py add --slug S --title T --priority P [--type idea|epic] - [--hook H] [--reason R] [--tag a,b] [--dir DIR] - создать задачу: файл + строка индекса - backlog.py edit S [--title T] [--hook H] [--type idea|epic|task] [--dir DIR] - сменить заголовок/хук/тип (файл + индекс) - backlog.py move S --priority P [--reason R] [--dir DIR] - перенести в другую секцию приоритета - backlog.py close S (--reason R | --implemented) [--dir DIR] - закрыть: --reason → кладбище + удаление, - --implemented → просто удаление (есть коммит) - backlog.py init [--dir DIR] [--sections "высокий,средний,низкий"] - завести пустой беклог в новом проекте - -Тело задачи (контекст, шаги, ссылки) остаётся агенту — add кладёт лишь заголовок, -мета-строку и плейсхолдер; агент дописывает тело редактором. - -Границы безопасности: слаг — только латиница kebab-case (traversal невозможен), ---dir обязан быть внутри рабочего каталога, в заголовок/хук/причину не пролезет -перевод строки, `·` в причине запрещён (это разделитель мета-полей). - -Язык не зашит инструментально: приоритеты сопоставляются с заголовками секций -индекса как есть. Текст задач — русский. -""" - -import argparse -import datetime -import os -import re -import subprocess -import sys -from pathlib import Path - -INDEX = "README.md" -CLOSED = "CLOSED.md" -SERVICE = {INDEX, CLOSED} - -META_FIELD = re.compile(r"^\*\*(.+?):\*\*\s*(.*)$") -INDEX_ENTRY = re.compile(r"^- \[(.+?)\]\((.+?\.md)\)\s*(?:—\s*(.*))?$") -SECTION = re.compile(r"^##\s+(.+?)\s*$") -TYPE_PREFIX = re.compile(r"^\[(.+?)\]\s*(.*)$") -SLUG_RE = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") -SLUG = re.compile(SLUG_RE.pattern + r"\.md") -# Строка кладбища: - ГГГГ-ММ-ДД `slug` — текст -CLOSED_ENTRY = re.compile(r"^- \d{4}-\d{2}-\d{2} `[a-z0-9-]+` — .+") - -TYPES = ("idea", "epic") # непустые типы-ключевые слова, префикс [..] в H1 -PLAIN_TYPE = "task" # обычная задача — без префикса -STALE_DAYS = 180 # порог «залежалась» для метрики здоровья в check - - -# --- Валидация недоверенного ввода (аргументы могут прийти из текста задачи) --- - -def bad_line(value: str, field: str) -> str | None: - """Однострочность: перевод строки/управляющий символ ломает индекс и файл.""" - if value is not None and (any(c in value for c in "\n\r") or any(ord(c) < 32 for c in value)): - return f"{field}: перевод строки или управляющий символ запрещён" - return None - - -def bad_slug(slug: str) -> str | None: - if not SLUG_RE.fullmatch(slug): - return f"слаг «{slug}» — только латиница kebab-case (без ../, точек, слэшей)" - return None - - -def bad_reason(reason: str | None) -> str | None: - if reason is None: - return None - if (e := bad_line(reason, "причина")): - return e - if "·" in reason: - return "причина: символ · зарезервирован под разделитель мета-полей" - return None - - -def dir_within_cwd(root: Path) -> bool: - try: - root.resolve().relative_to(Path.cwd().resolve()) - return True - except ValueError: - return False - - -# --- Атомарная запись: падение посреди write не оставит усечённый индекс --- - -def write_atomic(path: Path, text: str) -> None: - tmp = path.with_name(path.name + ".tmp") - tmp.write_text(text, encoding="utf-8") - os.replace(tmp, path) - - -def resolve_dir(explicit: str | None) -> Path: - """Каталог беклога для команд, кроме init. Явный --dir обязан быть внутри cwd.""" - if explicit: - root = Path(explicit) - if not dir_within_cwd(root): - sys.exit(f"--dir вне рабочего каталога: {explicit}") - if not (root / INDEX).is_file(): - sys.exit(f"беклога нет в «{explicit}» (нет {INDEX}); новый проект — backlog.py init") - return root - for candidate in ("docs/backlog", "backlog", "doc/backlog", "docs/tasks"): - if (Path(candidate) / INDEX).is_file(): - return Path(candidate) - sys.exit("каталог беклога не найден, укажи --dir" - " (искал: docs/backlog, backlog, doc/backlog, docs/tasks)") - - -def parse_index(root: Path) -> tuple[dict[str, dict], list[str]]: - """Строки индекса по имени файла + порядок секций (он же порядок приоритетов). - - Дубли имени файла тут схлопываются (побеждает последний) — их отдельно ловит - index_lint, поэтому опираться на этот dict как на полноту нельзя. - """ - entries: dict[str, dict] = {} - sections: list[str] = [] - section = None - for num, line in enumerate((root / INDEX).read_text(encoding="utf-8").splitlines(), 1): - m = SECTION.match(line) - if m: - section = m.group(1) - sections.append(section) - continue - m = INDEX_ENTRY.match(line) - if m: - title, target, hook = m.group(1), m.group(2), (m.group(3) or "").strip() - entries[target] = {"title": title, "section": section, "hook": hook, "line": num} - return entries, sections - - -def index_lint(root: Path) -> list[str]: - """Структурные дефекты индекса, которые схлопнутый dict parse_index не видит: - битые строки-пункты, дубли на один файл, задачи до первой секции приоритета.""" - errors: list[str] = [] - section = None - seen: dict[str, int] = {} - for num, line in enumerate((root / INDEX).read_text(encoding="utf-8").splitlines(), 1): - if SECTION.match(line): - section = SECTION.match(line).group(1) - continue - if not line.startswith("- ["): - continue - m = INDEX_ENTRY.match(line) - if not m: - errors.append(f"{INDEX}:{num}: строка-пункт не по формату" - f" «- [Заголовок](slug.md) — хук»") - continue - target = m.group(2) - if section is None: - errors.append(f"{INDEX}:{num}: {target} стоит до первой секции приоритета") - if target in seen: - errors.append(f"{INDEX}:{num}: дубль строки для {target}" - f" (первая — строка {seen[target]})") - else: - seen[target] = num - return errors - - -def parse_task(path: Path) -> dict: - text = path.read_text(encoding="utf-8") - lines = text.splitlines() - title = lines[0].removeprefix("#").strip() if lines and lines[0].startswith("#") else "" - kind, bare = PLAIN_TYPE, title - m = TYPE_PREFIX.match(title) - if m: - kind, bare = m.group(1).strip().lower(), m.group(2).strip() - # Мета-строка — первая непустая строка после заголовка (task-format.md). - # Поля разделены `·`, порядок свободный: приоритет распознаётся, где бы он ни - # стоял, а не только первым. Причина не должна содержать `·` — это разделитель. - meta = next((ln.strip() for ln in lines[1:] if ln.strip()), "") - priority, reason, tags = "", "", [] - if META_FIELD.match(meta): - for chunk in meta.split("·"): - f = META_FIELD.match(chunk.strip()) - if not f: - continue - key, value = f.group(1).strip().lower(), f.group(2).strip() - if key in ("приоритет", "priority"): - priority, _, reason = (p.strip() for p in value.partition("—")) - priority = priority.rstrip(".,").lower() - elif key in ("теги", "tags"): - tags = [t.strip().lower() for t in value.split(",") if t.strip()] - return {"title": title, "bare": bare, "type": kind, "priority": priority, - "reason": reason, "tags": tags, "path": path} - - -def tasks_of(root: Path) -> dict[str, dict]: - return {p.name: parse_task(p) for p in sorted(root.glob("*.md")) if p.name not in SERVICE} - - -def touched_map(root: Path) -> dict[str, str]: - """Дата последнего коммита для каждого файла беклога — одним вызовом git. - Ключ — имя файла (в каталоге беклога имена уникальны). Нет git / нет - истории → пустая карта, вызывающий подставит «—».""" - try: - out = subprocess.run(["git", "log", "--format=%as", "--name-only", "--", str(root)], - capture_output=True, text=True).stdout - except FileNotFoundError: - return {} - dates: dict[str, str] = {} - cur = None - for line in out.splitlines(): - if not line.strip(): - continue - if re.fullmatch(r"\d{4}-\d{2}-\d{2}", line): - cur = line # лог новейшие сверху → первая дата и есть последняя правка - elif cur: - dates.setdefault(os.path.basename(line), cur) - return dates - - -def check(root: Path, fix: bool = False) -> int: - if fix: - for line in apply_fixes(root): - print(f"ПОЧИНЕНО {line}") - print() - - entries, sections = parse_index(root) - tasks = tasks_of(root) - known = {s.lower() for s in sections} - errors: list[str] = [] - notes: list[str] = [] - - for name, task in tasks.items(): - entry = entries.get(name) - if not entry: - errors.append(f"{name}: файла нет в индексе {INDEX}") - if not SLUG.fullmatch(name): - errors.append(f"{name}: слаг не kebab-case латиницей") - if not task["title"]: - errors.append(f"{name}: нет заголовка H1") - if not task["priority"]: - errors.append(f"{name}: нет строки **Приоритет:**") - elif task["priority"] not in known: - errors.append(f"{name}: приоритет «{task['priority']}» не совпадает" - f" ни с одной секцией индекса ({', '.join(sections)})") - elif entry and entry["section"] and entry["section"].lower() != task["priority"]: - errors.append(f"{name}: приоритет в файле «{task['priority']}»," - f" а в индексе секция «{entry['section']}»") - if entry and entry["title"] != task["title"]: - errors.append(f"{name}: заголовок разошёлся\n" - f" файл: {task['title']}\n" - f" индекс: {entry['title']}") - if entry and not entry["hook"]: - notes.append(f"{name}: строка индекса без хука — по ней не выбрать задачу") - if task["type"] not in TYPES and task["type"] != PLAIN_TYPE: - notes.append(f"{name}: тип «{task['type']}» вне словаря" - f" ({'/'.join(TYPES)} или без префикса)") - if "" - write_atomic(path, f"# {title_full}\n\n{meta}\n\n{body}\n") - entry = f"- [{title_full}]({a.slug}.md)" + (f" — {a.hook}" if a.hook else "") - insert_entry(lines, section, entry) - save_index(root, lines) - print(f"создано: {a.slug}.md в секции «{section}»; допиши тело редактором") - if not a.hook: - print(f" без хука — задай: backlog.py edit {a.slug} --hook …") - return 0 - - -def cmd_edit(root: Path, a: argparse.Namespace) -> int: - for err in (bad_slug(a.slug), bad_line(a.title, "заголовок"), bad_line(a.hook, "хук")): - if err: - return fail(err) - if a.title is None and a.hook is None and a.type is None: - return fail("нечего менять: дай --title, --hook или --type") - path = root / f"{a.slug}.md" - if not path.exists(): - return fail(f"{a.slug}.md не найден") - lines = load_index(root) - ei = find_entry_index(lines, a.slug) - if ei is None: - return fail(f"строки индекса для {a.slug} нет") - task = parse_task(path) - if a.title is not None and not a.title.strip(): - return fail("пустой заголовок") - bare = a.title if a.title is not None else task["bare"] - kind = task["type"] if a.type is None else a.type.strip().lower() - prefix = "" if kind in ("", PLAIN_TYPE) else f"[{kind}] " - h1 = f"{prefix}{bare}" - flines = path.read_text(encoding="utf-8").splitlines() - if not flines or not flines[0].startswith("#"): - return fail(f"{a.slug}.md без заголовка H1 — прогони check") - flines[0] = f"# {h1}" - write_atomic(path, "\n".join(flines) + "\n") - m = INDEX_ENTRY.match(lines[ei]) - hook = a.hook if a.hook is not None else (m.group(3) or "").strip() - lines[ei] = f"- [{h1}]({a.slug}.md)" + (f" — {hook}" if hook else "") - save_index(root, lines) - print(f"{a.slug}: обновлено (заголовок/хук/тип)") - return 0 - - -def cmd_move(root: Path, a: argparse.Namespace) -> int: - for err in (bad_slug(a.slug), bad_reason(a.reason)): - if err: - return fail(err) - path = root / f"{a.slug}.md" - if not path.exists(): - return fail(f"{a.slug}.md не найден") - lines = load_index(root) - ei = find_entry_index(lines, a.slug) - if ei is None: - return fail(f"строки индекса для {a.slug} нет") - hi, section = find_section(lines, a.priority) - if hi is None: - avail = ", ".join(n for _, n in section_headers(lines)) - return fail(f"нет секции приоритета «{a.priority}» (есть: {avail})") - if not update_priority(path, section, a.reason): - return fail(f"{a.slug}.md без мета-строки **Приоритет:** — прогони check и почини") - entry = lines.pop(ei) - insert_entry(lines, section, entry) - save_index(root, lines) - print(f"{a.slug}: перенесено в «{section}»") - return 0 - - -def cmd_close(root: Path, a: argparse.Namespace) -> int: - for err in (bad_slug(a.slug), bad_reason(a.reason)): - if err: - return fail(err) - path = root / f"{a.slug}.md" - if not path.exists(): - return fail(f"{a.slug}.md не найден") - lines = load_index(root) - ei = find_entry_index(lines, a.slug) - if ei is None: - return fail(f"строки индекса для {a.slug} нет") - task = parse_task(path) - if a.reason: - reason = a.reason.rstrip() - dot = "" if reason.endswith((".", "!", "?")) else "." - date = datetime.date.today().isoformat() - bullet = (f"- {date} `{a.slug}` — {task['title']}. Причина: {reason}{dot}" - f" Был приоритет: {task['priority'] or '—'}.") - closed = root / CLOSED - prev = closed.read_text(encoding="utf-8") if closed.exists() else "# Кладбище беклога\n" - if not prev.endswith("\n"): - prev += "\n" - write_atomic(closed, prev + bullet + "\n") - # Порядок: индекс без строки → потом unlink. Обратный порядок оставил бы в - # индексе ссылку в никуда, если бы unlink упал. - lines.pop(ei) - save_index(root, lines) - path.unlink() - print(f"{a.slug}: {'на кладбище + удалено' if a.reason else 'удалено (реализовано)'}") - return 0 - - -def cmd_init(root: Path, a: argparse.Namespace) -> int: - if not dir_within_cwd(root): - return fail(f"--dir вне рабочего каталога: {root}") - index = root / INDEX - if index.exists(): - return fail(f"{index} уже есть — беклог заведён") - sections, seen = [], set() - for s in (s.strip() for s in a.sections.split(",")): - if s and s.lower() not in seen: - sections.append(s) - seen.add(s.lower()) - if not sections: - return fail("пустой список секций") - root.mkdir(parents=True, exist_ok=True) - preamble = ("# Беклог\n\n" - "Одна задача = один файл `.md` + строка в этом индексе.\n" - "Приоритет — грубая оценка «ценность / стоимость». Спекулятивные\n" - "задачи помечены `[idea]` в заголовке. Ведётся скиллом `backlog`.\n\n") - write_atomic(index, preamble + "".join(f"## {s}\n\n" for s in sections)) - closed = root / CLOSED - if not closed.exists(): - write_atomic(closed, - "# Кладбище беклога\n\n" - "Задачи, покинувшие беклог без реализации. Пишется `backlog.py close`.\n\n" - "\n") - print(f"беклог заведён: {root} (секции: {', '.join(sections)})") - return 0 - - -def main() -> int: - ap = argparse.ArgumentParser(prog="backlog.py") - sub = ap.add_subparsers(dest="command", required=True) - - p = sub.add_parser("check", help="согласованность файлов и индекса") - p.add_argument("--dir") - p.add_argument("--fix", action="store_true", - help="починить безопасный дрейф (секция, заголовок, дубли)") - - p = sub.add_parser("list", help="список задач") - p.add_argument("--dir") - p.add_argument("--stale", action="store_true") - p.add_argument("--priority") - p.add_argument("--type") - p.add_argument("--tag") - - p = sub.add_parser("add", help="создать задачу") - p.add_argument("--dir") - p.add_argument("--slug", required=True) - p.add_argument("--title", required=True) - p.add_argument("--priority", required=True) - p.add_argument("--type", choices=TYPES) - p.add_argument("--hook") - p.add_argument("--reason") - p.add_argument("--tag") - - p = sub.add_parser("edit", help="сменить заголовок/хук/тип") - p.add_argument("slug") - p.add_argument("--title") - p.add_argument("--hook") - p.add_argument("--type", choices=(*TYPES, PLAIN_TYPE)) - p.add_argument("--dir") - - p = sub.add_parser("move", help="перенести в другую секцию приоритета") - p.add_argument("slug") - p.add_argument("--priority", required=True) - p.add_argument("--reason") - p.add_argument("--dir") - - p = sub.add_parser("close", help="закрыть задачу (кладбище или удаление)") - p.add_argument("slug") - g = p.add_mutually_exclusive_group(required=True) - g.add_argument("--reason", help="причина отказа → строка на кладбище") - g.add_argument("--implemented", action="store_true", help="реализовано → просто удалить") - p.add_argument("--dir") - - p = sub.add_parser("init", help="завести пустой беклог") - p.add_argument("--dir") - p.add_argument("--sections", default="высокий,средний,низкий") - - a = ap.parse_args() - if a.command == "init": - return cmd_init(Path(a.dir or "docs/backlog"), a) - root = resolve_dir(a.dir) - dispatch = { - "check": lambda: check(root, a.fix), - "list": lambda: list_tasks(root, a), - "add": lambda: cmd_add(root, a), - "edit": lambda: cmd_edit(root, a), - "move": lambda: cmd_move(root, a), - "close": lambda: cmd_close(root, a), - } - return dispatch[a.command]() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index a5d20ac..7245f3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,9 +17,6 @@ package = false [tool.ruff] target-version = "py312" line-length = 88 -# backlog.py заморожен: плагин помечен УСТАРЕЛ и живёт до перевода последнего -# проекта, после чего удаляется целиком. Правки в него — риск без выгоды. -exclude = ["av-dev-backlog"] [tool.ruff.lint] select = [ diff --git a/scripts/copies.py b/scripts/copies.py index 342b37f..b1eac9f 100644 --- a/scripts/copies.py +++ b/scripts/copies.py @@ -46,7 +46,7 @@ from pathlib import Path OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4 -SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__", "av-dev-backlog"} +SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__"} # Идентификатор — только буквы, цифры и дефис. Строгость намеренная: она же # отличает **настоящий** маркер от примера в документации об этом механизме.