From 863769406f6597b64e852fbc0f882ad6b331d178 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Tue, 11 Aug 2026 10:35:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BA=D0=B0=D0=BD=D0=BE=D0=BD=2013:=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D1=91=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B2=D0=BB=D0=B0=D0=B4=D0=B5=D0=BB=D1=8C=D1=86=D1=83,=20?= =?UTF-8?q?=D1=83=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=20=D0=BF=D0=BE=D1=8F?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=D0=B0=D1=81=D1=8C=20=D1=81=D0=B2=D0=BE=D1=8F?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20=D1=84=D0=BE=D1=80?= =?UTF-8?q?=D0=BC=D0=B0=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Имя `.pm.json` пережило плагин `av-dev-pm` на два месяца и указывало в пустоту. Правило, которое из этого вынуто: имя служебного файла — имя плагина, который его завёл, и по нему же владельца узнают. - `docs/.pm.json` → `docs/.docs.json`, запись 13 журнала. Прежнее имя docs.py не читает намеренно: по этому числу upgrade решает, какие записи применять, и два дома разъехались бы молча ровно там, где это дороже всего. Вместо совместимости — узнавание: check видит старый файл и печатает готовую git mv - у каталога задач появилась своя версия формата — ключ `tasks` в `.tasks.json`, свой журнал версий и своё повышение. До сих пор её не было вовсе, хотя docs.py в комментарии уверенно на неё ссылался: описание опережало механику ровно так, как сказано в решении 195 - число своё, а не копия канонического: плагин ставится в одиночку, и у проекта без docs/ версии канона нет — сверять было бы не с чем - конфиг задач стал обязательным (init и adopt apply пишут его всегда), check сверяет число, `check --fix` его не приписывает: приписанное объявляло бы каталог приведённым к формату, шагов которого никто не делал - переезды 11 и 12 в новый журнал задним числом не переписаны — версия 1 велит догнать формат по журналу канона, называя признаки отставания поимённо (каталог в docs/tasks/, живой SPRINT.md) - запись 60 в DECISIONS со следствиями 200–203; отдельно разведено с решением F, где `.docs.json` отвергался как указатель путей: отвергнут был указатель, а не имя --- DECISIONS.md | 66 +++++++++ README.md | 11 +- TODO.md | 9 +- av-dev-code/agents/review-scope.md | 4 +- av-dev-code/skills/openspec/SKILL.md | 4 +- av-dev-code/skills/resolve/SKILL.md | 4 +- av-dev-code/skills/review/SKILL.md | 6 +- av-dev-docs/agents/doc-code-drift.md | 8 +- av-dev-docs/skills/canon/SKILL.md | 18 ++- av-dev-docs/skills/canon/references/canon.md | 29 ++-- .../skills/canon/references/changelog.md | 56 +++++++- .../skills/canon/references/skeletons.md | 13 +- av-dev-docs/skills/canon/scripts/docs.py | 60 ++++++--- av-dev-docs/skills/docs/SKILL.md | 4 +- av-dev-docs/skills/healthcheck/SKILL.md | 4 +- av-dev-docs/skills/init/SKILL.md | 9 +- av-dev-tasks/skills/tasks/SKILL.md | 59 ++++++-- av-dev-tasks/skills/tasks/references/adopt.md | 4 +- .../skills/tasks/references/changelog.md | 61 +++++++++ av-dev-tasks/skills/tasks/scripts/tasks.py | 127 +++++++++++++++--- scripts/addresses.py | 7 +- shared/plugin-boundary.md | 4 +- 22 files changed, 474 insertions(+), 93 deletions(-) create mode 100644 av-dev-tasks/skills/tasks/references/changelog.md diff --git a/DECISIONS.md b/DECISIONS.md index 227f30a..a4b92a2 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -3647,3 +3647,69 @@ change нет — берём источником актуальные спек которую никто не набрал, может не существовать вовсе — и именно так и было. 199. **Два дома у факта расходятся не когда-нибудь, а сразу.** Из четырёх пар описаний плагина совпала одна — та, которую с момента заведения не правили. + +## 60. Служебный файл зовётся по плагину-владельцу; у задач появилась своя версия формата (2026-08-11) + +Файл версии канона звался `docs/.pm.json` — по плагину `av-dev-pm`, который +распался на четыре ещё в решении 56 и которого больше нет. Имя пережило +владельца на два месяца и указывало в пустоту: читающий его искал плагин, о +котором в репозитории не осталось ни строки. Переименован в `docs/.docs.json` +записью 13 журнала канона. + +**Правило, которое из этого вынуто и теперь держит все три файла:** имя +служебного файла — имя плагина, который его завёл. `.docs.json` — канон, +`.tasks.json` — задачи, `openspec/config.yaml` — конвейер. По этому же следу +скиллы узнают, что сосед в проекте работал, и правило перестало быть просто +перечнем — оно выводимо. + +**Прежнее имя `docs.py` не читает.** Соблазн «прочитать оба и не мешать людям» +здесь стоит дороже, чем везде: по этому числу `upgrade` решает, какие записи +журнала применять, и два дома для него разъехались бы молча в том самом месте, +где расхождение и вредно. Вместо совместимости — узнавание: `check` видит файл +под старым именем и печатает готовую команду `git mv`. + +**У каталога задач появилась своя версия формата** — ключ `tasks` в +`<каталог задач>/.tasks.json` и свой журнал версий в скилле `av-dev-tasks:tasks`. +До сих пор её не было вовсе, хотя `docs.py` в комментарии уверенно ссылался на +«свою версию формата» соседа: описание опережало механику ровно так, как описано +в решении 195. Формат задач при этом менялся — записями 8, 11 и 12 чужого +журнала. + +**Число именно своё, а не копия канонического.** Плагин ставится в одиночку: +проект, взявший учёт работ без канона документов, каталога `docs/` не имеет +вовсе, а значит не имеет и версии канона — сверять было бы не с чем. Копия +чужого числа в `tasks.py` была бы вторым домом одной версии и разъехалась бы при +первом же обновлении одного плагина без другого. + +**Переезды, случившиеся до появления числа, задним числом в новый журнал не +переписаны.** Версия 1 — это формат на день её появления; что проекту нужно было +пройти до неё, названо шагом «догнать формат по журналу канона» с поимёнными +признаками отставания (каталог в `docs/tasks/`, живой `SPRINT.md`). Второй +перечень тех же шагов разошёлся бы с первым — это ровно та ошибка, из-за которой +план однажды повторял записи версий 3, 4 и 5 построчно. + +**Конфиг задач стал обязательным.** Раньше он заводился только ради имён, +отличных от умолчания, и проект с умолчаниями жил без файла вовсе. Версия — не +настройка, от которой можно отказаться, поэтому `init` и `adopt apply` пишут его +всегда, а `check` требует числа. + +Отдельно стоит сказать, чтобы не спутали при чтении журнала: `.docs.json` +однажды уже был отвергнут — решением F, но **как указатель путей**. Отвергнут +был указатель, а не имя; сегодняшний файл путями проекта не распоряжается, он +объявляет версию и называет то немногое, чего из раскладки не вывести. + +### Что из этого следует + +200. **Имя служебного файла — часть границы плагинов, а не деталь.** Оно + называет владельца, и по нему же владельца узнают. Пережившее владельца имя + врёт дважды: указывает на несуществующее и прячет того, кто файл ведёт на + самом деле. +201. **Версия нужна каждому формату, который живёт в чужом репозитории.** Без + числа «приведён ли проект» не имеет определённого ответа, и отставший + каталог выглядит здоровым до первой команды, которая об него споткнётся. +202. **Своя версия — у своего плагина, всегда.** Общее число на два плагина + переживает ровно до первого проекта, где поставлен один из них. +203. **Версию двигают руками, и это не слабость проверки.** Число отвечает + на вопрос «по какой записи повышать», а не «сделаны ли шаги по существу». + Машина, приписывающая недостающее число сама, объявляет проект приведённым + к формату, которого никто не проходил. diff --git a/README.md b/README.md index 0f4de04..6eb8924 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,16 @@ OpenSpec скиллом `av-dev-code:openspec`, `av-dev-docs:docs` берёт у Прийти в старый проект и перевести его на канон — `/av-dev-docs:canon`. Канон версионируется, и проекты повышаются по [журналу -версий](av-dev-docs/skills/canon/references/changelog.md). +версий](av-dev-docs/skills/canon/references/changelog.md); версия проекта живёт в +`docs/.docs.json`. + +**Версий две, и они независимы.** У каталога задач своя — ключ `tasks` в +`<каталог задач>/.tasks.json`, свой [журнал +версий](av-dev-tasks/skills/tasks/references/changelog.md) и своё повышение +скиллом `/av-dev-tasks:tasks`. Плагины ставятся порознь: у проекта, взявшего учёт +работ без канона документов, `docs/` нет вовсе, и общее число оказалось бы домом, +которого у половины проектов не существует. Имя служебного файла при этом +называет владельца — `.docs.json`, `.tasks.json`, `openspec/config.yaml`. ## Подключение diff --git a/TODO.md b/TODO.md index 10ec2b9..ba398b3 100644 --- a/TODO.md +++ b/TODO.md @@ -20,7 +20,7 @@ цикл SDD, конвейер ревью, OpenSpec), `av-dev-git`. Общее, что нужно нескольким дословно, живёт домом в `shared/` и уезжает копиями. -Канон документов — **версия 12**. Живые проекты стоят на 2–3 и на плагине +Канон документов — **версия 13**; формат задач — **версия 1**, своя и со своим журналом. Живые проекты стоят на 2–3 и на плагине `av-dev-pm`, которого больше нет. Бумажная часть закрыта аудитом четырёх плагинов и четырьмя пропусками правок @@ -42,11 +42,12 @@ - [ ] удалить проектные копии: `.claude/skills/healthlog-{task,review}-pipeline` и девять `.claude/agents/healthlog-review-*.md`. Они прошлого поколения и после переезда указывают на документы, которых уже не будет -- [ ] `av-dev-docs:canon` в режиме `adopt` — он приведёт проект к канону 12 +- [ ] `av-dev-docs:canon` в режиме `adopt` — он приведёт проект к канону 13 сразу, картой и с подтверждением. Файл-в-файл здесь не расписан: раскладку знает скилл, и второй перечень разошёлся бы с ним -- [ ] каталог задач — в `tasks/` корня (канон 11), не в `docs/tasks/`, и без - `SPRINT.md` (канон 12). Скилл задач зовётся из `adopt` сам +- [ ] каталог задач — в `tasks/` корня (канон 11), не в `docs/tasks/`, без + `SPRINT.md` (канон 12) и с версией формата в `tasks/.tasks.json` (журнал + задач, версия 1). Скилл задач зовётся из `adopt` сам - [ ] гейт проекта: три шага вместо одного — `docs.py check`, `tasks.py check --dir tasks`, `openspec.py check`. **Второй и третий раньше не были нужны:** согласованность задач тянул за собой `docs.py`, форму `config.yaml` diff --git a/av-dev-code/agents/review-scope.md b/av-dev-code/agents/review-scope.md index 3bd3590..26f9b3e 100644 --- a/av-dev-code/agents/review-scope.md +++ b/av-dev-code/agents/review-scope.md @@ -117,7 +117,7 @@ color: green |---|---|---| | **тема** | `conventions.*`, `security.*`, `architecture.*`, любой свой документ проекта | заводишь строку темы и назначаешь исполнителя | | **источник темы** | `passport.*`, `database.*` | называешь адресом **внутри** строки чужой темы, своей строки не заводишь | -| **процессный** | `tasks/`, `review.*`, `adr.*`, `research.*`, `.pm.json` | называешь строкой «процессный», исполнителя нет и не должно быть | +| **процессный** | `tasks/`, `review.*`, `adr.*`, `research.*`, `.docs.json` | называешь строкой «процессный», исполнителя нет и не должно быть | `docs/review.*` при этом ты читаешь — но как **настройку конвейера**, откуда берутся вопросы по темам и триггеры метки, а не как тему. `adr.*` и `research.*` @@ -128,7 +128,7 @@ color: green **Каждая запись в `docs/` обязана попасть в план строкой своей категории.** Не «я посмотрел и решил» — перечислением. Это и есть проверка твоей работы: план сверяется с `ls docs/` за секунду, и пропущенный документ виден без рассуждения. -`docs/.pm.json` — единственное исключение: служебный файл, не документ, в плане +`docs/.docs.json` — единственное исключение: служебный файл, не документ, в плане не упоминается. **Категории `источник` и `процессный` закрыты — они перечислены выше поимённо.** diff --git a/av-dev-code/skills/openspec/SKILL.md b/av-dev-code/skills/openspec/SKILL.md index cfd1a39..07b9ecf 100644 --- a/av-dev-code/skills/openspec/SKILL.md +++ b/av-dev-code/skills/openspec/SKILL.md @@ -147,8 +147,10 @@ python3 $os form # слепок формы против жив **Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня установленных плагинов проект не ведёт — он разошёлся бы с действительностью -молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` — +молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.docs.json` — канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер. +Имя файла — имя плагина, который его завёл, и держит он в том числе версию своего +формата: у канона документов и у каталога задач они свои и двигаются порознь. diff --git a/av-dev-code/skills/resolve/SKILL.md b/av-dev-code/skills/resolve/SKILL.md index 1c817e9..41e7c2c 100644 --- a/av-dev-code/skills/resolve/SKILL.md +++ b/av-dev-code/skills/resolve/SKILL.md @@ -57,8 +57,10 @@ description: "Решить одну задачу от постановки до **Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня установленных плагинов проект не ведёт — он разошёлся бы с действительностью -молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` — +молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.docs.json` — канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер. +Имя файла — имя плагина, который его завёл, и держит он в том числе версию своего +формата: у канона документов и у каталога задач они свои и двигаются порознь. diff --git a/av-dev-code/skills/review/SKILL.md b/av-dev-code/skills/review/SKILL.md index ac52add..3740331 100644 --- a/av-dev-code/skills/review/SKILL.md +++ b/av-dev-code/skills/review/SKILL.md @@ -88,8 +88,10 @@ description: "Конвейер ревью изменения, устроенны **Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня установленных плагинов проект не ведёт — он разошёлся бы с действительностью -молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` — +молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.docs.json` — канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер. +Имя файла — имя плагина, который его завёл, и держит он в том числе версию своего +формата: у канона документов и у каталога задач они свои и двигаются порознь. @@ -113,7 +115,7 @@ description: "Конвейер ревью изменения, устроенны |---|---|---| | **тема** | заводит направление проверки и требует исполнителя | `conventions.*`, `security.*`, `architecture.*`, свои документы проекта | | **источник темы** | читает как материал чужой темы, своей не порождает | `passport.*`, `database.*`, `CLAUDE.md`/`AGENTS.md`, `openspec/specs/` | -| **процессный документ** | не судит по нему изменение | `tasks/`, `docs/review.*`, `docs/adr.*`, `docs/research.*`, `docs/.pm.json` | +| **процессный документ** | не судит по нему изменение | `tasks/`, `docs/review.*`, `docs/adr.*`, `docs/research.*`, `docs/.docs.json` | **Одна процессная запись всё же читается — `docs/review.*`.** В ней лежит настройка самого конвейера: вопросы по темам, журнал дефектов, типовые узлы, diff --git a/av-dev-docs/agents/doc-code-drift.md b/av-dev-docs/agents/doc-code-drift.md index cc7d03f..9d9e301 100644 --- a/av-dev-docs/agents/doc-code-drift.md +++ b/av-dev-docs/agents/doc-code-drift.md @@ -1,6 +1,6 @@ --- name: doc-code-drift -description: "Сверка документов канона с кодом по закрытому перечню проверяемых фактов: имя основной ветки и команды из CLAUDE.md, запреты с путями, testdata и временный каталог, путь миграций из .pm.json, внешние зависимости поимённо в architecture.md против манифеста, настройки с числовым значением в database.md против конфига и кода, единые точки проекта против реального числа реализаций, capability против существующих модулей. Отвечает на «этот факт ещё верен», а не «эта архитектура правильная». Читает весь репозиторий, гоняет только читающие команды. Отдаёт готовые формулировки и ничего не правит сам. Согласованность документов между собой смотрит агент doc-consistency. Зовётся скиллом av-dev-docs:healthcheck — на весь канон разом; он же зовётся шагом adopt и шагом upgrade. Только чтение." +description: "Сверка документов канона с кодом по закрытому перечню проверяемых фактов: имя основной ветки и команды из CLAUDE.md, запреты с путями, testdata и временный каталог, путь миграций из .docs.json, внешние зависимости поимённо в architecture.md против манифеста, настройки с числовым значением в database.md против конфига и кода, единые точки проекта против реального числа реализаций, capability против существующих модулей. Отвечает на «этот факт ещё верен», а не «эта архитектура правильная». Читает весь репозиторий, гоняет только читающие команды. Отдаёт готовые формулировки и ничего не правит сам. Согласованность документов между собой смотрит агент doc-consistency. Зовётся скиллом av-dev-docs:healthcheck — на весь канон разом; он же зовётся шагом adopt и шагом upgrade. Только чтение." tools: Read, Grep, Glob, Bash model: sonnet color: green @@ -37,7 +37,7 @@ color: green ## Что тебе дают -Корень проекта. Читаешь `CLAUDE.md`, `docs/**`, `docs/.pm.json`, +Корень проекта. Читаешь `CLAUDE.md`, `docs/**`, `docs/.docs.json`, `openspec/specs/**` — и репозиторий: манифесты зависимостей, конфиги, файлы сборки и CI, дерево пакетов. @@ -62,7 +62,7 @@ color: green держит прежнее имя. 3. **Пути** — все, которые канон обязывает называть: `migrations` из - `docs/.pm.json`, `testdata`, временный каталог, пути в запретах `CLAUDE.md`. + `docs/.docs.json`, `testdata`, временный каталог, пути в запретах `CLAUDE.md`. Проверка: существует ли. Путь в запрете, которого нет, — находка **особого рода**: запрет, который не на что наложить, читается как соблюдённый, а на деле охраняет пустоту, пока настоящий каталог зовётся иначе. @@ -147,7 +147,7 @@ color: green ``` факт источник проверено чем итог имя основной ветки CLAUDE.md git branch сошлось -путь миграций docs/.pm.json ls РАЗОШЛОСЬ +путь миграций docs/.docs.json ls РАЗОШЛОСЬ внешние зависимости architecture.md go.mod 2 не названы единые точки: парсер входа architecture.md grep по формату сошлось настройки БД database.md — не проверено diff --git a/av-dev-docs/skills/canon/SKILL.md b/av-dev-docs/skills/canon/SKILL.md index 9abe9c9..547f6d6 100644 --- a/av-dev-docs/skills/canon/SKILL.md +++ b/av-dev-docs/skills/canon/SKILL.md @@ -116,8 +116,10 @@ capability: незаполненный канон это переходное с **Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня установленных плагинов проект не ведёт — он разошёлся бы с действительностью -молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` — +молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.docs.json` — канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер. +Имя файла — имя плагина, который его завёл, и держит он в том числе версию своего +формата: у канона документов и у каталога задач они свои и двигаются порознь. @@ -176,7 +178,7 @@ capability), `openspec/config.yaml`. Порядок важен — он минимизирует окно, в котором ссылки битые: -1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть; +1. `docs/.docs.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть; 2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**: незаполненное — одной честной информативной строкой, а не «TBD»; 3. **OpenSpec, если его нет или `config.yaml` остался примером** — **вызови @@ -265,7 +267,7 @@ capability), `openspec/config.yaml`. 3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии проекта до текущей и делай названное в каждой записи. Записи независимы и применяются по порядку. -4. Подними `canon` в `docs/.pm.json` до текущей. +4. Подними `canon` в `docs/.docs.json` до текущей. 5. `docs.py check`. 6. **Позови судей** — Skill `av-dev-docs:healthcheck`. 7. **Позови вычитку** — агент `doc-wording`, но **только по тем документам, @@ -276,7 +278,15 @@ capability), `openspec/config.yaml`. Записи журнала описывают **что сделать проекту**. Если запись этого не говорит — это дефект журнала, и о нём надо сказать, а не догадываться. -**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.pm.json` с +**Каталог задач повышается своим журналом, а не этим.** У него своя версия +формата — ключ `tasks` в `<каталог задач>/.tasks.json`, — и двигает её плагин +`av-dev-tasks`. Запись канона вправе сказать «позови соседа», но не вправе +двигать чужое число: две версии, которые ходят по одному журналу, разъезжаются +на первом же проекте, поставившем один плагин без другого. Отстал каталог +задач — это скажет `tasks.py check` своей строкой гейта, а повысит скилл +`av-dev-tasks:tasks`. + +**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.docs.json` с версией скрипта — и только его. Применена ли запись журнала **по существу**, он не знает: проект несёт `"canon": 6` и может не иметь того, чего требовала любая из пройденных версий. Записи применяются руками (переименовать секцию, проставить diff --git a/av-dev-docs/skills/canon/references/canon.md b/av-dev-docs/skills/canon/references/canon.md index 0ef79b7..f5c243d 100644 --- a/av-dev-docs/skills/canon/references/canon.md +++ b/av-dev-docs/skills/canon/references/canon.md @@ -65,7 +65,7 @@ CLAUDE.md памятка агенту: что это, ст severity, команды, семантика гейта, запреты AGENTS.md необязателен, лежит рядом; читается теми же docs/ - .pm.json версия канона и пути, нужные проверкам + .docs.json версия канона и пути, нужные проверкам passport.md | passport/ зачем и для кого; чем НЕ является; сценарии architecture.md | architecture/ как сложено — обзор; окружение и эксплуатация database.md | database/ схема хранилища; представление данных и настройки @@ -119,7 +119,7 @@ openspec/ | `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) | | `adr.*` | процессный | — | | `research.*` | процессный | — | -| `.pm.json` | процессный | — (служебный файл, не документ) | +| `.docs.json` | процессный | — (служебный файл, не документ) | **Список тем открытый, и это не послабление, а механизм.** Категории `источник` и `процессный` **закрыты** — они перечислены здесь поимённо и @@ -354,8 +354,10 @@ kebab-case.** Причина не эстетическая: имя файла с ### `tasks/` **Каталог задач канону не принадлежит.** Его ведёт отдельный плагин -`av-dev-tasks` — своим скриптом, своим конфигом `<каталог>/.tasks.json` и своей -версией формата. Канон **резервирует место** в `docs/` и внутрь не смотрит: +`av-dev-tasks` — своим скриптом, своим конфигом `<каталог>/.tasks.json`, своей +версией формата в нём же и своим журналом версий. Канон о том числе не +высказывается и его не двигает: повышает каталог задач тот, кто его ведёт. +Канон **резервирует место** в `docs/` и внутрь не смотрит: `docs.py` каталог не открывает, его отсутствия не считает дрейфом и согласованность задач не проверяет. Проект, поставивший только канон документов, задач не ведёт вовсе, и отказом это быть не может. @@ -546,7 +548,7 @@ kebab-case.** Причина не эстетическая: имя файла с архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт правдоподобную труху вместо находок. -## `docs/.pm.json` +## `docs/.docs.json` ```json { @@ -562,12 +564,23 @@ kebab-case.** Причина не эстетическая: имя файла с `migrations` — путь каталога миграций, если БД есть; по нему `docs.py` делает сверку с `database.md`. +**Имя файла — имя плагина, который его завёл.** Канон документов ведёт +`av-dev-docs`, поэтому `.docs.json`; у каталога задач по тому же правилу +`.tasks.json`, у конвейера — `openspec/config.yaml`. До версии 13 файл звался +`.pm.json` — по плагину `av-dev-pm`, который распался на четыре и которого +больше нет; имя пережило владельца и указывало в пустоту. Прежнее имя `docs.py` +не читает: два дома для одной версии канона расходятся молча, а переименование +стоит одну команду (версия 13 журнала, и `check` называет её сам, когда видит +старый файл). + **Ключа `tasks` здесь больше нет.** Настройки каталога задач вернулись в свой файл `<каталог задач>/.tasks.json`, потому что ведёт их другой плагин: конфиг, лежащий в `docs/`, был бы домом, которого нет у проекта, поставившего учёт работ -без канона документов. Состав ключей описывает тот плагин, а не канон. Прежний -ключ читается, пока живы непереехавшие проекты, и `tasks.py` говорит о нём -замечанием на каждом прогоне — версия 8 журнала просит его убрать. +без канона документов. Состав ключей описывает тот плагин, а не канон. Там же — +**версия формата задач**, и она своя: у проекта без `docs/` версии канона нет +вовсе, сверять её было бы не с чем. Прежний ключ читается, пока живы +непереехавшие проекты, и `tasks.py` говорит о нём замечанием на каждом +прогоне — версия 8 журнала просит его убрать. Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py` игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом diff --git a/av-dev-docs/skills/canon/references/changelog.md b/av-dev-docs/skills/canon/references/changelog.md index 7fc9f64..f222b08 100644 --- a/av-dev-docs/skills/canon/references/changelog.md +++ b/av-dev-docs/skills/canon/references/changelog.md @@ -1,8 +1,15 @@ # Журнал версий канона -Одна запись на версию. Проект знает свою версию из `docs/.pm.json`; `canon +Одна запись на версию. Проект знает свою версию из `docs/.docs.json`; `canon upgrade` идёт по записям снизу вверх от версии проекта до текущей и делает то, -что в них названо. +что в них названо. Записи ниже версии 13 зовут этот файл прежним именем, +`docs/.pm.json`, — так и было на день записи, и переписывать историю мы не +станем; переименование делает запись 13. + +**Каталог задач этим журналом не повышается.** У него своя версия формата и свой +журнал — `references/changelog.md` скилла `av-dev-tasks:tasks`. Записи 8, 11 и 12 +трогали его в те времена, когда своего числа у него не было; впредь запись канона +вправе позвать соседа, но не двигать его версию. Правило записи: **что добавилось, что переехало, что удалено, что сделать проекту**. Без последнего пункта запись бесполезна — по ней и работает @@ -13,6 +20,51 @@ upgrade` идёт по записям снизу вверх от версии п --- +## Версия 13 — 2026-08-11 + +Служебный файл канона переименован: `docs/.pm.json` → `docs/.docs.json`. Имя +досталось от плагина `av-dev-pm`, который распался на четыре и которого больше +нет: файл пережил владельца и указывал в пустоту. Правило простое и теперь +соблюдается всеми тремя: **имя служебного файла — имя плагина, который его +завёл**, `.docs.json` — канон, `.tasks.json` — задачи, `openspec/config.yaml` — +конвейер. + +**Что изменилось.** `docs.py` читает только новое имя. Прежнее он не читает +намеренно: два дома для одной версии канона расходятся молча, а тут расхождение +стоило бы дорого — по этому числу `upgrade` решает, какие записи применять. +Файл под старым именем `check` узнаёт и называет отдельной строкой с готовой +командой, а не жалуется на пропажу. + +**Что появилось у соседа.** У каталога задач теперь есть **своя версия +формата** — ключ `tasks` в `<каталог задач>/.tasks.json`, — и свой журнал версий +в скилле `av-dev-tasks:tasks`. До сих пор её не было вовсе: формат задач менялся +записями этого журнала (8, 11, 12), хотя каталог принадлежит другому плагину и +ставится без канона документов. Канон это число не двигает. + +**Что сделать проекту.** + +1. `git mv docs/.pm.json docs/.docs.json` — одним коммитом с шагом 2. Содержимое + не меняется: ключи те же. +2. **Поправить упоминания прежнего имени** в своих файлах — `CLAUDE.md`, гейт, + `README.md`, `docs/**`. Битой ссылкой это чаще всего не выглядит (файл + служебный, на него ссылаются прозой), поэтому `docs.py check` таких упоминаний + не ловит: ищи `grep -rn '\.pm\.json'` по репозиторию. +3. **Объявить версию формата задач**, если каталог задач в проекте есть: + `<каталог задач>/.tasks.json` с ключом `"tasks": <версия>`. Файла нет вовсе — + заведи, он теперь обязателен: версия не настройка, от которой можно + отказаться. Какое число ставить и что сделать перед этим, говорит журнал + владельца — **позови скилл `av-dev-tasks:tasks`**, здесь этих шагов нет + намеренно: второй перечень чужих шагов разошёлся бы с первым. +4. Гейт не меняется: шаги те же, версию задач сторожит `tasks.py check`, который + в нём уже стоит. +5. `docs/.docs.json`: `"canon": 13`. + +**Чего делать не надо.** Ключи в файле не трогаются, документы не переезжают, +записи задач не меняются: версия 13 — про имена служебных файлов и про то, кто +чью версию двигает. + +--- + ## Версия 12 — 2026-08-09 Спринты отменены. Работа идёт задача за задачей, и замороженный набор перестал diff --git a/av-dev-docs/skills/canon/references/skeletons.md b/av-dev-docs/skills/canon/references/skeletons.md index e384f28..f4a837f 100644 --- a/av-dev-docs/skills/canon/references/skeletons.md +++ b/av-dev-docs/skills/canon/references/skeletons.md @@ -117,7 +117,7 @@ со строкой «запись лежит сжатой и распаковывается целиком». ``` -Нет БД — файла нет, и в `docs/.pm.json` нет ключа `migrations`. +Нет БД — файла нет, и в `docs/.docs.json` нет ключа `migrations`. ## `docs/security.md` @@ -438,7 +438,7 @@ severity стоит здесь, а не выводится каждым прох говорит (единственный дом, а не форма) — [canon.md](canon.md), раздел `openspec/config.yaml`. -## `docs/.pm.json` +## `docs/.docs.json` ```json { @@ -452,5 +452,10 @@ severity стоит здесь, а не выводится каждым прох плейсхолдер ломает разбор JSON громко, а отставшее число дало бы дрейф молча. Плюс `"migrations": "<путь>"`, если есть БД. Ключа `"tasks"` здесь **нет**: -настройки каталога задач переехали в свой файл `<каталог задач>/.tasks.json`, -потому что ведёт их другой плагин. Состав ключей — [canon.md](canon.md). +настройки каталога задач и версия их формата переехали в свой файл `<каталог +задач>/.tasks.json`, потому что ведёт их другой плагин. Состав ключей — +[canon.md](canon.md). + +Имя файла — по плагину-владельцу, `av-dev-docs`. До версии 13 он звался +`.pm.json`, по распавшемуся `av-dev-pm`; проект с прежним именем `docs.py check` +называет отдельной строкой и зовёт переименовать. diff --git a/av-dev-docs/skills/canon/scripts/docs.py b/av-dev-docs/skills/canon/scripts/docs.py index 4173249..4c6e72c 100644 --- a/av-dev-docs/skills/canon/scripts/docs.py +++ b/av-dev-docs/skills/canon/scripts/docs.py @@ -25,10 +25,19 @@ from dataclasses import dataclass, field from pathlib import Path from typing import NoReturn -CANON_VERSION = 12 +CANON_VERSION = 13 OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4 +# Дом версии канона и путей, нужных проверкам. Имя — от плагина, который файл +# завёл: настройки канона документов ведёт `av-dev-docs`, и файл называется по +# нему. Прежнее имя досталось от `av-dev-pm` — плагина, который распался на +# четыре и которого больше нет; читать его скрипт не умеет намеренно, потому что +# два дома для версии канона расходятся молча, а переименование стоит одну +# команду и названо записью 13 журнала. +CONFIG = "docs/.docs.json" +LEGACY_CONFIG = "docs/.pm.json" + # --- Раскладка канона ------------------------------------------------------- # Документ канона: имя → (категория, на какой вопрос отвечает). @@ -59,7 +68,7 @@ DOCS = { "review": ("процессный", "настройка конвейера + журнал дефектов"), } -# Документ, обязательный только при условии: имя → (ключ .pm.json, категория, +# Документ, обязательный только при условии: имя → (ключ .docs.json, категория, # пояснение). CONDITIONAL_DOCS = { "database": ("migrations", "источник", "схема хранилища и настройки"), @@ -68,7 +77,7 @@ CONDITIONAL_DOCS = { # Обязательные файлы вне раскладки docs/. REQUIRED = { "CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта", - "docs/.pm.json": "версия канона и пути, нужные проверкам", + CONFIG: "версия канона и пути, нужные проверкам", } # Файлы, которые документ-каталог обязан держать сверх README.md. @@ -77,15 +86,19 @@ DOC_EXTRA = { } # Служебное в docs/ и каталог задач, оставшийся там от прежней раскладки. Формы -# у них скрипт не проверяет, и по разным причинам: `.pm.json` не markdown, а +# у них скрипт не проверяет, и по разным причинам: `.docs.json` не markdown, а # задачи **принадлежат другому плагину** — `av-dev-tasks`, со своим скриптом, -# своим конфигом и своей версией формата. +# своим конфигом и своей версией формата (её сторожит `tasks.py check`). # # Дом задач с версии 11 — `tasks/` в корне репозитория, то есть вне `docs/` # вовсе. `docs/tasks/` здесь терпится потому, что непереехавший проект не должен # получать «файл вне канона» вдобавок к записи журнала, которая и так велит ему # переехать. Внутрь скрипт не смотрит ни в том, ни в другом случае. -NOT_DOCS = {".pm.json", "tasks"} +# +# Прежнее имя конфига терпится ровно за тем же: про переименование проект +# слышит одну строку — от `check_required`, — а не две, из которых вторая ещё и +# зовёт файл лишним. +NOT_DOCS = {".docs.json", ".pm.json", "tasks"} # Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое. Имена, # совпадающие с темой, отсюда убраны намеренно: `docs/conventions.md` и @@ -251,15 +264,15 @@ def fail(code: int, msg: str) -> NoReturn: def read_config(root: Path, rep: Report) -> dict: - path = root / "docs" / ".pm.json" + path = root / CONFIG if not path.exists(): return {} try: data = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: - fail(ENV, f"docs/.pm.json не разбирается: {exc}") + fail(ENV, f"{CONFIG} не разбирается: {exc}") if not isinstance(data, dict): - fail(ENV, "docs/.pm.json должен быть объектом") + fail(ENV, f"{CONFIG} должен быть объектом") return data @@ -267,14 +280,14 @@ def read_config(root: Path, rep: Report) -> dict: def check_version(root: Path, cfg: dict, rep: Report) -> None: - if not (root / "docs" / ".pm.json").exists(): + if not (root / CONFIG).exists(): return # об отсутствии файла скажет check_required, второй раз не нужно if "canon" not in cfg: - rep.error("в docs/.pm.json нет ключа canon — версия канона не объявлена") + rep.error(f"в {CONFIG} нет ключа canon — версия канона не объявлена") return got = cfg["canon"] if not isinstance(got, int): - rep.error(f"canon в docs/.pm.json должен быть целым числом, а не {got!r}") + rep.error(f"canon в {CONFIG} должен быть целым числом, а не {got!r}") return if got < CANON_VERSION: rep.error( @@ -316,8 +329,21 @@ def doc_home(root: Path, name: str) -> tuple[Path | None, str | None]: def check_required(root: Path, cfg: dict, rep: Report) -> None: for rel, what in REQUIRED.items(): - if not (root / rel).exists(): - rep.error(f"нет {rel} — {what}") + if (root / rel).exists(): + continue + # Файл под прежним именем — это не «нет файла», а незаконченный переезд, + # и чинится он одной командой. Без этой ветки проект услышал бы «нет + # версии канона» и пошёл заводить второй файл рядом с первым. + if rel == CONFIG and (root / LEGACY_CONFIG).exists(): + rep.error( + f"нет {rel} — {what}. Настройки лежат под прежним именем" + f" {LEGACY_CONFIG} (от плагина av-dev-pm, которого больше нет):" + f" `git mv {LEGACY_CONFIG} {rel}` — журнал канона, версия 13." + f" Прежнее имя не читается, поэтому в этом прогоне всё" + f" остальное проверено так, будто настроек нет вовсе" + ) + continue + rep.error(f"нет {rel} — {what}") for name, (kind, what) in DOCS.items(): home, complaint = doc_home(root, name) @@ -342,10 +368,10 @@ def check_required(root: Path, cfg: dict, rep: Report) -> None: rep.error( f"нет документа {name} (docs/{name}.md или docs/{name}/)," f" категория «{kind}» — {what}" - f" (обязателен: в .pm.json объявлен {key})" + f" (обязателен: в .docs.json объявлен {key})" ) elif key not in cfg and home is None: - rep.skip(f"{name} — в .pm.json нет ключа {key}, проверка неприменима") + rep.skip(f"{name} — в .docs.json нет ключа {key}, проверка неприменима") def check_stray(root: Path, rep: Report) -> None: @@ -527,7 +553,7 @@ def changed_files(root: Path, base: str, rep: Report) -> list[str] | None: def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None: migrations = cfg.get("migrations") if not migrations: - rep.skip("в .pm.json нет ключа migrations — сверка со схемой неприменима") + rep.skip("в .docs.json нет ключа migrations — сверка со схемой неприменима") return if not base: rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась") diff --git a/av-dev-docs/skills/docs/SKILL.md b/av-dev-docs/skills/docs/SKILL.md index 648d937..428d9d3 100644 --- a/av-dev-docs/skills/docs/SKILL.md +++ b/av-dev-docs/skills/docs/SKILL.md @@ -159,8 +159,10 @@ description: Вести содержимое документов канона **Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня установленных плагинов проект не ведёт — он разошёлся бы с действительностью -молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` — +молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.docs.json` — канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер. +Имя файла — имя плагина, который его завёл, и держит он в том числе версию своего +формата: у канона документов и у каталога задач они свои и двигаются порознь. diff --git a/av-dev-docs/skills/healthcheck/SKILL.md b/av-dev-docs/skills/healthcheck/SKILL.md index 6a8acc1..ccc3789 100644 --- a/av-dev-docs/skills/healthcheck/SKILL.md +++ b/av-dev-docs/skills/healthcheck/SKILL.md @@ -60,8 +60,10 @@ check` и его скрипт; здесь начинается там, где к **Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня установленных плагинов проект не ведёт — он разошёлся бы с действительностью -молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` — +молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.docs.json` — канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер. +Имя файла — имя плагина, который его завёл, и держит он в том числе версию своего +формата: у канона документов и у каталога задач они свои и двигаются порознь. diff --git a/av-dev-docs/skills/init/SKILL.md b/av-dev-docs/skills/init/SKILL.md index dbcf477..740b3ed 100644 --- a/av-dev-docs/skills/init/SKILL.md +++ b/av-dev-docs/skills/init/SKILL.md @@ -26,7 +26,7 @@ description: "Завести новый проект — сессия вопро | `passport.md` | `architecture.md` | | `CLAUDE.md` | `database.md` | | `security.md` | `conventions/` | -| `docs/.pm.json` | `research/`, `adr/` | +| `docs/.docs.json` | `research/`, `adr/` | | | `review.md` — журнал пуст, настройка появится с первым ревью | Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет, @@ -97,8 +97,10 @@ description: "Завести новый проект — сессия вопро **Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня установленных плагинов проект не ведёт — он разошёлся бы с действительностью -молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` — +молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.docs.json` — канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер. +Имя файла — имя плагина, который его завёл, и держит он в том числе версию своего +формата: у канона документов и у каталога задач они свои и двигаются порознь. @@ -117,7 +119,8 @@ description: "Завести новый проект — сессия вопро **Вызов не разрешился** — проект без конвейера живёт без OpenSpec законно: строка доклада, и дальше; `docs.py check` о каталоге тоже промолчит. -4. Заведи `docs/.pm.json` с текущей версией канона. +4. Заведи `docs/.docs.json` с текущей версией канона — число берётся из + `docs.py version`, а не из памяти. 5. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и отдельным файлом не остаётся: два дома для одного замысла разойдутся на первом же уточнении. diff --git a/av-dev-tasks/skills/tasks/SKILL.md b/av-dev-tasks/skills/tasks/SKILL.md index 0bb427c..7d5a268 100644 --- a/av-dev-tasks/skills/tasks/SKILL.md +++ b/av-dev-tasks/skills/tasks/SKILL.md @@ -1,6 +1,6 @@ --- name: tasks -description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). У каждой записи есть тип (goal, feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. Заведение записи из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, штурм сырья, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Расстановка приоритетов и разбор накопившегося — скилл groom. Не реализует задачи — этим занимается скилл решения задачи. +description: Ведение задач и целей как каталога markdown-файлов (одна запись = один файл в items/ + строка в одном из индексов). У каждой записи есть тип (goal, feature, fix, chore, research), и тип решает, каких разделов она требует и что с ней можно делать. Заведение записи из диалога, разбор находок аудита/ревью, декомпозиция на независимо полезные части, штурм сырья, гигиена полей и проверка согласованности индексов. Использовать, когда просят добавить задачу/идею/цель, превратить находки ревью в задачи, разбить задачу, проработать идею, поправить формат или проверить беклог. Он же повышает каталог до текущей версии формата по своему журналу версий, когда tasks.py check говорит, что каталог отстал. Расстановка приоритетов и разбор накопившегося — скилл groom. Не реализует задачи — этим занимается скилл решения задачи. --- # Задачи @@ -399,7 +399,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап | 0 | сошлось / сделано | дальше по сценарию | | 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать | | 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу | -| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `docs/.pm.json`, повтор не поможет | +| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `<каталог задач>/.tasks.json`, повтор не поможет | | 4 | внутренний сбой | дефект скрипта, доложить | Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога @@ -486,6 +486,39 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап [fix](references/task-fix.md) · [chore](references/task-chore.md) · [research](references/task-research.md). +## Версия формата + +Формат каталога задач меняется, и проект должен знать, к какой его версии +приведён. Число живёт ключом `tasks` в `<каталог задач>/.tasks.json`, журнал +версий — [references/changelog.md](references/changelog.md), сверяет их +`tasks.py check`: отстало — строка расхождения, ушло вперёд — устарел плагин. + +**Версия своя, а не канона документов.** Плагин ставится в одиночку: проект, +взявший учёт работ без `av-dev-docs`, каталога `docs/` не имеет вовсе, а значит +не имеет и версии канона — сверять было бы не с чем. Обратной совместимости у +формата нет: есть «приведён» и «не приведён». + +**`upgrade` — повысить каталог до текущего формата:** + +1. `python3 $tk check --dir D` — первая же строка расхождений называет версию + проекта и версию скрипта. Проект новее скрипта — **обнови маркетплейс**, а не + проект: это отстал плагин. +2. Иди по [журналу](references/changelog.md) снизу вверх от версии проекта до + текущей и делай названное в каждой записи. Записи независимы и применяются по + порядку. +3. Подними `tasks` в `.tasks.json` до текущей — руками, последним шагом. Раньше + времени поднятое число объявляет каталог приведённым к формату, шагов + которого никто не делал; `check --fix` этого не пишет намеренно. +4. `check --dir D` ещё раз — до отсутствия расхождений. + +Записи журнала описывают **что сделать проекту**. Если запись этого не говорит — +это дефект журнала, и о нём надо сказать, а не догадываться. + +**Канон документов сюда не вмешивается.** Его журнал двигает своё число в +`docs/.docs.json` и вправе сказать «позови этот скилл», но не двигать версию +формата задач: две версии, ходящие по одному журналу, разъедутся на первом же +проекте, где стоит один плагин без другого. + ## Сценарии ### Завести запись из диалога @@ -647,17 +680,19 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап действительно новый, а перевод чужой раскладки делает `av-dev-docs:canon`. У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и полагаться на него скилл не должен: молча найденный чужой каталог это дрейф. -- **Настройки живут в `<каталог задач>/.tasks.json`** — свой файл у своего - плагина: **имена** файлов и заголовков, и только если они отличаются от - умолчания. Неизвестный ключ — код 3 на любой команде, так что лишнее слово в - этом объекте останавливает работу с задачами целиком. +- **Версия формата и настройки живут в `<каталог задач>/.tasks.json`** — свой + файл у своего плагина: ключ `tasks` с версией формата плюс **имена** файлов и + заголовков, и последние — только если отличаются от умолчания. Неизвестный + ключ — код 3 на любой команде, так что лишнее слово в этом объекте + останавливает работу с задачами целиком. - Дом именно свой, а не `docs/.pm.json`, потому что `docs/` принадлежит плагину - канона: проект, поставивший учёт работ без него, каталога `docs/` не имеет - вовсе. Прежний ключ `tasks` в `docs/.pm.json` читается, **только когда своего - файла нет** — для проектов, заведённых до раскола плагинов; скрипт при этом - говорит замечанием, куда его перенести. Есть оба — побеждает свой, и об этом - тоже говорится вслух: молча выбранный из двух конфиг это дрейф. + Дом именно свой, а не `docs/.docs.json`, потому что `docs/` принадлежит + плагину канона: проект, поставивший учёт работ без него, каталога `docs/` не + имеет вовсе. Прежний ключ `tasks` в `docs/.pm.json` читается, **только когда + своего файла нет** — для проектов, заведённых до раскола плагинов; скрипт при + этом говорит замечанием, куда его перенести. Есть оба — побеждает свой, и об + этом тоже говорится вслух: молча выбранный из двух конфиг это дрейф. Версию + прежний дом не знает и знать не может — она читается только из своего файла. - **Секции беклога** берутся из заголовков `##` индекса как есть; их количество и названия — дело проекта (умолчание `Ядро` / `Инфра`). **В конфиге их нет** — второй список разошёлся бы с заголовками молча. diff --git a/av-dev-tasks/skills/tasks/references/adopt.md b/av-dev-tasks/skills/tasks/references/adopt.md index 55811ea..c2bbfc9 100644 --- a/av-dev-tasks/skills/tasks/references/adopt.md +++ b/av-dev-tasks/skills/tasks/references/adopt.md @@ -66,7 +66,9 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \ всегда `tasks`. Секции беклога (`--sections`) — по умолчанию `Ядро,Инфра`; если у проекта деление другое по существу, оно называется здесь, а не подгоняется под умолчание, и становится **заголовками `##` - индекса** — их единственным домом. В `docs/.pm.json` секции не пишутся. + индекса** — их единственным домом. В `.tasks.json` секции не пишутся: там + версия формата и имена частей, а второй список секций разошёлся бы с + заголовками молча. 2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два прохода дадут два несогласованных состояния. 3. **Заполни карту**: `slug` (английский), `section`, `goal` у каждой записи; diff --git a/av-dev-tasks/skills/tasks/references/changelog.md b/av-dev-tasks/skills/tasks/references/changelog.md new file mode 100644 index 0000000..aa19cd5 --- /dev/null +++ b/av-dev-tasks/skills/tasks/references/changelog.md @@ -0,0 +1,61 @@ +# Журнал версий формата задач + +Одна запись на версию. Проект знает свою версию из ключа `tasks` в `<каталог +задач>/.tasks.json`; повышение (`upgrade` в [SKILL.md](../SKILL.md), раздел +«Версия формата») идёт по записям снизу вверх от версии проекта до текущей и +делает то, что в них названо. + +Правило записи: **что добавилось, что переехало, что удалено, что сделать +проекту**. Без последнего пункта запись бесполезна — по ней и работает +повышение. + +Версия — целое число. Обратной совместимости у формата нет: есть «приведён» и «не +приведён». + +**Это журнал формата задач, а не канона документов.** Числа у них разные и +двигаются порознь: плагин `av-dev-tasks` ставится в одиночку, и у проекта без +`av-dev-docs` версии канона нет вовсе. Журнал канона — +`references/changelog.md` скилла `av-dev-docs:canon`. + +--- + +## Версия 1 — 2026-08-11 + +Первая объявленная версия формата. До неё каталог задач версии не имел вовсе: +формат менялся, а сказать, к какому его состоянию приведён конкретный проект, +было нечем — `tasks.py` о расхождении молчал, и отставший каталог выглядел +здоровым ровно до первой команды, которая об него спотыкалась. + +**Что появилось.** Ключ `tasks` в `<каталог задач>/.tasks.json` — целое число, +версия формата. Сам файл стал **обязательным**: до сих пор он заводился только +ради имён, отличных от умолчания, и проект с умолчаниями жил без него. Версия — +не настройка, от которой можно отказаться, поэтому `init` и `adopt apply` теперь +пишут файл всегда, а `check` требует числа и сверяет его со своим. + +**Что версия значит, а что нет.** Она отвечает на один вопрос — «по какой записи +журнала повышать каталог». Что записи применены **по существу**, из числа не +следует: двигают его руками, и соврать им так же легко, как любой другой +строкой. `check --fix` недостающее число не приписывает намеренно — это было бы +объявлением каталога приведённым к формату, шагов которого никто не делал. + +**Чего в этой записи нет.** Переезды, случившиеся до появления числа, — каталог +из `docs/` в корень (канон 11) и отмена спринтов (канон 12) — задним числом сюда +не переписаны. Они уже названы журналом канона, и второй перечень тех же шагов +разошёлся бы с первым. Версия 1 — это формат на день её появления, что бы +проекту ни пришлось пройти до неё. + +**Что сделать проекту.** + +1. **Догнать формат по журналу канона, если каталог отстал.** Признаки известны + поимённо: каталог лежит в `docs/tasks/` (канон 11 велит `git mv docs/tasks + tasks` и починку относительных ссылок внутри записей), в нём есть `SPRINT.md` + или теги `sprint:<слаг>` (канон 12 велит снести файл, вернуть строки в беклог + через `check --fix` и расставить порядок грумингом). Ничего из этого нет — + каталог уже в сегодняшнем формате, и шаг пропускается. +2. **Завести `<каталог задач>/.tasks.json`**, если его нет. Имена частей в него + не переписываются: там только то, что отличается от умолчания. +3. **Записать версию**: `"tasks": 1` первым ключом. +4. `tasks.py check --dir <каталог задач>` — до отсутствия расхождений. + +**Что при этом не трогается.** Записи в `items/`, индексы и `REJECTED.md` не +меняются ни строкой: версия 1 объявляет то, что уже есть, а не переделывает его. diff --git a/av-dev-tasks/skills/tasks/scripts/tasks.py b/av-dev-tasks/skills/tasks/scripts/tasks.py index 28300fe..05a577a 100755 --- a/av-dev-tasks/skills/tasks/scripts/tasks.py +++ b/av-dev-tasks/skills/tasks/scripts/tasks.py @@ -10,7 +10,8 @@ Раскладка. Путь каталога — `tasks/` в корне репозитория, жёстко. Каталог принадлежит этому плагину, а не канону документов: `docs/` ведёт другой плагин, и проект, поставивший учёт работ без него, каталога `docs/` не имеет вовсе. Имена -внутри настраиваются через `tasks/.tasks.json`. +внутри и **версия формата** живут в `tasks/.tasks.json`; журнал версий — +references/changelog.md рядом со скриптом. tasks/ items/ задачи и цели файлами, .md @@ -107,8 +108,26 @@ import subprocess import sys from pathlib import Path -CONFIG_NAME = ".tasks.json" # дом настроек: свой файл в каталоге задач -PM_CONFIG_REL = "../.pm.json" # прежний дом: docs/.pm.json, ключ "tasks" +CONFIG_NAME = ".tasks.json" # дом настроек и версии: свой файл в каталоге +PM_CONFIG_REL = "../.pm.json" # прежний дом настроек: docs/.pm.json, ключ "tasks" + +# Версия формата задач — **своя, а не канона документов**. Число живёт ключом +# `tasks` в `.tasks.json`, журнал версий — references/changelog.md рядом со +# скриптом, повышает его операция `upgrade` скилла `av-dev-tasks:tasks`. +# +# Число именно своё, потому что плагин ставится в одиночку: проект, взявший учёт +# работ без канона документов, каталога `docs/` не имеет вовсе, а значит не имеет +# и версии канона — сверять было бы не с чем. Копия чужого числа в этом скрипте +# была бы вторым домом для одной версии и разъехалась бы молча при обновлении +# одного плагина без другого. +# +# Переезды каталога задач, случившиеся до появления этого числа (в корень — +# канон 11, отмена спринтов — канон 12), задним числом сюда не переписаны: они +# уже названы журналом канона, и второй перечень тех же шагов разошёлся бы с +# первым. Версия 1 — формат на день её появления, что бы проекту ни пришлось +# пройти до неё. +FORMAT_VERSION = 1 +VERSION_KEY = "tasks" EXIT_OK = 0 EXIT_DRIFT = 1 @@ -433,7 +452,7 @@ class Layout: def load_config(root: Path) -> dict: - """Настройки каталога задач. + """Настройки каталога задач и версия его формата. Дом — `<каталог задач>/.tasks.json`: **свой файл у своего плагина**. Ключ `tasks` в `docs/.pm.json` читается, пока живы проекты, заведённые до раскола @@ -445,6 +464,10 @@ def load_config(root: Path) -> dict: плагину. Проект, поставивший учёт задач без канона документов, каталога `docs/` не имеет вовсе, и дом настроек, лежащий в чужом дереве, был бы домом, которого у половины проектов нет. + + Версия формата (ключ `tasks`) читается **только из своего файла**: прежний + дом её не знал и знать не может, и молча выведенная из его отсутствия версия + была бы догадкой о том, что чинится одной строкой. """ path = root / CONFIG_NAME pm = (root / PM_CONFIG_REL).resolve() @@ -485,7 +508,7 @@ def _read_json(path: Path) -> dict: def _validate_config(data: dict, path: Path) -> dict: - unknown = set(data) - set(DEFAULTS) + unknown = set(data) - set(DEFAULTS) - {VERSION_KEY} # Ключ «plan» был домом оглавления целей до того, как файл стал ROADMAP.md. # Без этой ветки проект со старым конфигом получал бы «неизвестный ключ» и # искал опечатку там, где на самом деле переименование канона. @@ -495,9 +518,19 @@ def _validate_config(data: dict, path: Path) -> dict: f" av-dev-docs:canon (upgrade), а не правь ключ в одиночку:" f" файл и ссылки на него переезжают вместе с ним") if unknown: + known = sorted({*DEFAULTS, VERSION_KEY}) raise Env(f"{path}: неизвестные ключи: {', '.join(sorted(unknown))}" - f" (известны: {', '.join(sorted(DEFAULTS))})") + f" (известны: {', '.join(known)})") + # Версия — единственный ключ-число: остальные это имена файлов и заголовков. + # Битое число тут останавливает работу целиком (код 3), а не идёт дрейфом, + # потому что «на какой версии формата каталог» решает, чему верить дальше. + got = data.get(VERSION_KEY) + if VERSION_KEY in data and (isinstance(got, bool) or not isinstance(got, int)): + raise Env(f"{path}: ключ «{VERSION_KEY}» — версия формата задач," + f" ожидалось целое число, а не {got!r}") for key, value in data.items(): + if key == VERSION_KEY: + continue if not isinstance(value, str) or not value.strip(): raise Env(f"{path}: ключ «{key}» — ожидалась непустая строка") if key in PATH_KEYS and (value.startswith("/") or ".." in Path(value).parts): @@ -536,10 +569,46 @@ def config_problems(lay: Layout) -> list[str]: return out +def version_problems(lay: Layout) -> list[str]: + """Версия формата задач: объявлена ли и та ли, которую знает скрипт. + + Отвечает на один вопрос — «по какой записи журнала повышать каталог», — и + ни на какой другой. Что запись оформлена по правилам своей версии, отсюда не + следует: число двигает тот, кто прошёл шаги, и соврать им так же легко, как + любой другой строкой. Цена вранья при этом низкая, а польза от вопроса есть + ровно там, где формат поменялся, а каталог остался прежним. + + `check --fix` этого не чинит намеренно: приписать недостающее число значило + бы объявить каталог приведённым к формату, шагов которого никто не делал. + Заводит число `init`, двигает — операция `upgrade` скилла. + """ + path = lay.root / CONFIG_NAME + # Прежний дом (`docs/.pm.json`) версии не знает, поэтому спрашиваем строго + # свой файл: «конфиг нашёлся» и «версия объявлена» это разные события. + if not path.is_file(): + return [f"нет {path} — версия формата задач не объявлена." + f" Заведи файл с «{VERSION_KEY}»: {FORMAT_VERSION} (журнал версий —" + f" references/changelog.md скилла av-dev-tasks:tasks)"] + # Что число целое, уже проверил `_validate_config` — иначе сюда не дошли бы + # вовсе (код 3). Здесь `isinstance` значит ровно «ключ есть». + got = lay.cfg.get(VERSION_KEY) + if not isinstance(got, int): + return [f"{path}: нет ключа «{VERSION_KEY}» — версия формата не объявлена," + f" текущая {FORMAT_VERSION}"] + if got < FORMAT_VERSION: + return [f"каталог приведён к формату версии {got}, текущая —" + f" {FORMAT_VERSION}: нужно повышение по журналу" + f" (скилл av-dev-tasks:tasks, операция upgrade)"] + if got > FORMAT_VERSION: + return [f"каталог приведён к формату версии {got}, а скрипт знает" + f" {FORMAT_VERSION}: устарел плагин, обнови маркетплейс"] + return [] + + def looks_like_tasks(p: Path) -> bool: if (p / CONFIG_NAME).is_file(): return True - try: # индекс мог быть переименован через docs/.pm.json + try: # индекс мог быть переименован через конфиг name = load_config(p).get("backlog", DEFAULTS["backlog"]) except Env: name = DEFAULTS["backlog"] @@ -1027,7 +1096,10 @@ def check(lay: Layout, fix: bool = False) -> int: entries = {k: v[0] for k, v in idx.items()} sections = {k: v[1] for k, v in idx.items()} tasks = tasks_of(lay) - errors: list[str] = [] + # Версия формата идёт первой строкой расхождений: остальные находки читаются + # иначе, когда каталог отстал от формата, — часть из них тогда не дрейф, а + # непройденный шаг журнала. + errors: list[str] = version_problems(lay) notes: list[str] = [] label = {k: lay.name(k) for k in lay.indexes} @@ -2524,13 +2596,15 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]: def init_files(lay: Layout, sections: list[str], roadmap_sections: list[str], cfg: dict) -> dict[Path, str]: out: dict[Path, str] = {} - if cfg: - # Пишем всегда в свой `.tasks.json`, даже когда рядом живёт - # `docs/.pm.json`: дом настроек принадлежит этому плагину, а `docs/` — - # другому, и его в проекте может не быть. load_config читает свой файл - # первым, так что записанное сюда и прочитается отсюда. - out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False, - indent=2) + "\n" + # Файл заводится всегда, даже когда все имена умолчательные: в нём живёт + # версия формата, а версия — не настройка, от которой можно отказаться. + # + # Пишем всегда в свой `.tasks.json`, даже когда рядом живёт `docs/.pm.json`: + # дом настроек принадлежит этому плагину, а `docs/` — другому, и его в + # проекте может не быть. load_config читает свой файл первым, так что + # записанное сюда и прочитается отсюда. + out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False, + indent=2) + "\n" out[lay.index("backlog")] = ( "# Беклог\n\n" f"Что **можно взять**. Одна задача = один файл `{lay.cfg['items']}/.md`\n" @@ -2589,9 +2663,15 @@ def uniq_sections(raw: str) -> list[str]: def cmd_init(root: Path, a: argparse.Namespace) -> int: if not dir_within_cwd(root): raise Usage(f"--dir вне рабочего каталога: {root}") - cfg = {k: v for k, v in (("items", a.items), ("backlog", a.backlog), ("roadmap", a.roadmap), - ("rejected", a.rejected)) if v} - lay = Layout(root, cfg) + names = {k: v for k, v in (("items", a.items), ("backlog", a.backlog), + ("roadmap", a.roadmap), ("rejected", a.rejected)) if v} + # Версия формата — первым ключом и всегда: каталог, заведённый сегодня, + # приведён к сегодняшнему формату, и объявить это должен тот, кто его завёл. + # Имена частей — следом и только те, что названы явно: умолчание, записанное + # в файл, стало бы вторым домом для того же имени. В раскладку версия не + # идёт — `Layout` про имена, и число среди имён там ничего не значит. + cfg = {VERSION_KEY: FORMAT_VERSION, **names} + lay = Layout(root, names) if lay.index("backlog").exists(): raise Usage(f"{lay.index('backlog')} уже есть — каталог задач заведён") @@ -2615,8 +2695,9 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int: print(f"каталог задач заведён: {root}") print(f" секции беклога: {', '.join(sections)};" f" секции роадмапа канонические: {', '.join(roadmap_sections)}") - if cfg: - print(f" имена частей записаны в {root / CONFIG_NAME}") + what = ("версия формата и имена частей записаны" if names + else "версия формата записана") + print(f" {what} в {root / CONFIG_NAME}: формат {FORMAT_VERSION}") return EXIT_OK @@ -2974,7 +3055,11 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int: # --- план записи --- wr = Plan() - for path, text in init_files(lay, sections, roadmap_sections, {}).items(): + # Версия та же, что у `init`: каталог выводится из чужой раскладки сегодня и + # сегодняшним форматом, сколько бы лет ни было тому, из чего он выведен. + # Имён частей здесь нет — адаптация раскладывает всё по умолчаниям. + for path, text in init_files(lay, sections, roadmap_sections, + {VERSION_KEY: FORMAT_VERSION}).items(): wr.file(path, text) backlog_lines = init_files(lay, sections, roadmap_sections, {})[lay.index("backlog")].splitlines() roadmap_lines = init_files(lay, sections, roadmap_sections, {})[lay.index("roadmap")].splitlines() diff --git a/scripts/addresses.py b/scripts/addresses.py index cf26294..ed69bd1 100644 --- a/scripts/addresses.py +++ b/scripts/addresses.py @@ -57,6 +57,7 @@ OWNERS = { # Адрес, верный на момент записи, здесь останется навсегда, и это не дрейф. JOURNALS = { "av-dev-docs/skills/canon/references/changelog.md": "журнал версий канона", + "av-dev-tasks/skills/tasks/references/changelog.md": "журнал версий формата задач", "DECISIONS.md": "журнал решений", "HISTORY.md": "журнал работ", "NOTES.md": "рабочие заметки", @@ -102,7 +103,7 @@ def stem(name: str) -> str: """Имя документа без формы: файл, каталог и `.*` — один и тот же адрес. Форму дома канон оставляет проекту: `docs/security.md` и `docs/security/` - называют одно. Скрытые имена (`.pm.json`) остаются как есть — точка в них + называют одно. Скрытые имена (`.docs.json`) остаются как есть — точка в них не расширение. """ name = name.rstrip(".") @@ -119,7 +120,7 @@ def vocabularies(root: Path) -> tuple[dict[str, set[str]], dict[str, str]]: docs_names = {stem(n) for n in docs.DOCS} docs_names |= {stem(n) for n in docs.CONDITIONAL_DOCS} docs_names |= {stem(n) for n in docs.NOT_DOCS} - # `docs/.pm.json` объявлен обязательным файлом вне раскладки. + # `docs/.docs.json` объявлен обязательным файлом вне раскладки. docs_names |= {stem(Path(p).name) for p in docs.REQUIRED if p.startswith("docs/")} tasks_names = {stem(tasks.DEFAULTS[k]) for k in tasks.PATH_KEYS} @@ -201,7 +202,7 @@ def main() -> int: if own_themes: print(f"Имён вне перечня {len(own_themes)}, и они **не судятся** —" f" список тем открытый: {', '.join(sorted(own_themes))}.") - print(f"Не проверялось: журналы ({len(JOURNALS)} файла — они описывают" + print(f"Не проверялось: журналы ({len(JOURNALS)} шт. — они описывают" f" прошлые состояния), адреса `openspec/*` (раскладка чужого" f" инструмента, у нас владельца нет), упоминания в комментариях" f" скриптов — сверяется только markdown.") diff --git a/shared/plugin-boundary.md b/shared/plugin-boundary.md index 3d2ceb2..ec15bf9 100644 --- a/shared/plugin-boundary.md +++ b/shared/plugin-boundary.md @@ -51,7 +51,9 @@ **Присутствие узнаётся вызовом или следом в проекте, но не объявлением.** Перечня установленных плагинов проект не ведёт — он разошёлся бы с действительностью -молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.pm.json` — +молча. Что сосед здесь работал, видно по заведённому им файлу: `docs/.docs.json` — канон, `<каталог задач>/.tasks.json` — задачи, `openspec/config.yaml` — конвейер. +Имя файла — имя плагина, который его завёл, и держит он в том числе версию своего +формата: у канона документов и у каталога задач они свои и двигаются порознь.