From 95c9499f06abd0bc64517bbcb40f980f1fff359d Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Thu, 13 Aug 2026 10:30:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3:=20=D0=BE?= =?UTF-8?q?=D0=B4=D0=BD=D0=B0=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D0=B4=D0=B8=D0=BD=20=D1=81=D0=BB=D1=83=D0=B6?= =?UTF-8?q?=D0=B5=D0=B1=D0=BD=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB,=20.a?= =?UTF-8?q?v-dev.toml=20=D0=B2=20=D0=BA=D0=BE=D1=80=D0=BD=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Версий было две — канон 14 в docs/.docs.json и формат задач 1 в <каталог задач>/.tasks.json, — и порознь они двигались потому, что плагины ставились порознь. Плагин один, версия одна и начинается с 1; журналы обеих прежних нумераций закрыты и лежат рядом непереписанными, действующий журнал открывается записью о слиянии с перечнем шагов проекту. Формат TOML взят ради комментариев: файл живёт в репозитории проекта, и назначение числа читают из него самого. Отсюда правило записи — скрипты правят строку, а не переписывают файл. Читатель общий, shared/config.py: два разбора одной схемы были бы двумя домами. Каталог задач перестал узнаваться служебным файлом и называется ключом [tasks] dir; узнают его по индексу. Прежние файлы не читаются — увидев их, docs.py и tasks.py называют прежнюю раскладку и зовут upgrade. --- av-dev/agents/doc-code-drift.md | 8 +- av-dev/agents/review-scope.md | 4 +- av-dev/shared/config.py | 228 +++++ av-dev/skills/code-review/SKILL.md | 2 +- av-dev/skills/doc-canon/SKILL.md | 34 +- av-dev/skills/doc-canon/references/canon.md | 69 +- .../references/changelog-before-merge.md | 784 ++++++++++++++++ .../changelog-tasks-before-merge.md} | 23 +- .../skills/doc-canon/references/changelog.md | 846 ++---------------- .../skills/doc-canon/references/skeletons.md | 40 +- av-dev/skills/doc-canon/scripts/docs.py | 127 +-- av-dev/skills/doc-init/SKILL.md | 4 +- av-dev/skills/task-track/SKILL.md | 68 +- av-dev/skills/task-track/references/adopt.md | 2 +- av-dev/skills/task-track/scripts/tasks.py | 321 +++---- scripts/addresses.py | 7 +- 16 files changed, 1450 insertions(+), 1117 deletions(-) create mode 100644 av-dev/shared/config.py create mode 100644 av-dev/skills/doc-canon/references/changelog-before-merge.md rename av-dev/skills/{task-track/references/changelog.md => doc-canon/references/changelog-tasks-before-merge.md} (74%) diff --git a/av-dev/agents/doc-code-drift.md b/av-dev/agents/doc-code-drift.md index c0f75c2..0764dcc 100644 --- a/av-dev/agents/doc-code-drift.md +++ b/av-dev/agents/doc-code-drift.md @@ -1,6 +1,6 @@ --- name: doc-code-drift -description: "Сверка документов канона с кодом по закрытому перечню проверяемых фактов: имя основной ветки и команды из CLAUDE.md, запреты с путями, testdata и временный каталог, путь миграций из .docs.json, внешние зависимости поимённо в architecture.md против манифеста, настройки с числовым значением в database.md против конфига и кода, единые точки проекта против реального числа реализаций, capability против существующих модулей. Отвечает на «этот факт ещё верен», а не «эта архитектура правильная». Читает весь репозиторий, гоняет только читающие команды. Отдаёт готовые формулировки и ничего не правит сам. Согласованность документов между собой смотрит агент doc-consistency. Зовётся скиллом av-dev:doc-healthcheck — на весь канон разом; он же зовётся шагом adopt и шагом upgrade. Только чтение." +description: "Сверка документов канона с кодом по закрытому перечню проверяемых фактов: имя основной ветки и команды из CLAUDE.md, запреты с путями, testdata и временный каталог, путь миграций из .av-dev.toml, внешние зависимости поимённо в architecture.md против манифеста, настройки с числовым значением в database.md против конфига и кода, единые точки проекта против реального числа реализаций, capability против существующих модулей. Отвечает на «этот факт ещё верен», а не «эта архитектура правильная». Читает весь репозиторий, гоняет только читающие команды. Отдаёт готовые формулировки и ничего не правит сам. Согласованность документов между собой смотрит агент doc-consistency. Зовётся скиллом av-dev:doc-healthcheck — на весь канон разом; он же зовётся шагом adopt и шагом upgrade. Только чтение." tools: Read, Grep, Glob, Bash model: sonnet color: green @@ -37,7 +37,7 @@ color: green ## Что тебе дают -Корень проекта. Читаешь `CLAUDE.md`, `docs/**`, `docs/.docs.json`, +Корень проекта. Читаешь `CLAUDE.md`, `docs/**`, `.av-dev.toml`, `openspec/specs/**` — и репозиторий: манифесты зависимостей, конфиги, файлы сборки и CI, дерево пакетов. @@ -62,7 +62,7 @@ color: green держит прежнее имя. 3. **Пути** — все, которые канон обязывает называть: `migrations` из - `docs/.docs.json`, `testdata`, временный каталог, пути в запретах `CLAUDE.md`. + `.av-dev.toml`, `testdata`, временный каталог, пути в запретах `CLAUDE.md`. Проверка: существует ли. Путь в запрете, которого нет, — находка **особого рода**: запрет, который не на что наложить, читается как соблюдённый, а на деле охраняет пустоту, пока настоящий каталог зовётся иначе. @@ -147,7 +147,7 @@ color: green ``` факт источник проверено чем итог имя основной ветки CLAUDE.md git branch сошлось -путь миграций docs/.docs.json ls РАЗОШЛОСЬ +путь миграций .av-dev.toml ls РАЗОШЛОСЬ внешние зависимости architecture.md go.mod 2 не названы единые точки: парсер входа architecture.md grep по формату сошлось настройки БД database.md — не проверено diff --git a/av-dev/agents/review-scope.md b/av-dev/agents/review-scope.md index 8f65443..58ca574 100644 --- a/av-dev/agents/review-scope.md +++ b/av-dev/agents/review-scope.md @@ -117,7 +117,7 @@ color: green |---|---|---| | **тема** | `conventions.*`, `security.*`, `architecture.*`, любой свой документ проекта | заводишь строку темы и назначаешь исполнителя | | **источник темы** | `passport.*`, `database.*` | называешь адресом **внутри** строки чужой темы, своей строки не заводишь | -| **процессный** | `tasks/`, `review.*`, `adr.*`, `research.*`, `.docs.json` | называешь строкой «процессный», исполнителя нет и не должно быть | +| **процессный** | `tasks/`, `review.*`, `adr.*`, `research.*`, `.av-dev.toml` | называешь строкой «процессный», исполнителя нет и не должно быть | `docs/review.*` при этом ты читаешь — но как **настройку конвейера**, откуда берутся вопросы по темам и триггеры метки, а не как тему. `adr.*` и `research.*` @@ -128,7 +128,7 @@ color: green **Каждая запись в `docs/` обязана попасть в план строкой своей категории.** Не «я посмотрел и решил» — перечислением. Это и есть проверка твоей работы: план сверяется с `ls docs/` за секунду, и пропущенный документ виден без рассуждения. -`docs/.docs.json` — единственное исключение: служебный файл, не документ, в плане +`.av-dev.toml` — единственное исключение: служебный файл, не документ, в плане не упоминается. **Категории `источник` и `процессный` закрыты — они перечислены выше поимённо.** diff --git a/av-dev/shared/config.py b/av-dev/shared/config.py new file mode 100644 index 0000000..b77c912 --- /dev/null +++ b/av-dev/shared/config.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Служебный файл проекта `.av-dev.toml`: чтение, запись, конверсия старого. + +**Это дом.** Файл один на весь плагин, поэтому и читатель у него один: `docs.py` +и `tasks.py` берут настройки отсюда, а не каждый своим разбором. Два разбора +одного формата — это два дома для одной схемы, и расходятся они молча: первым +разъезжается не значение ключа, а то, что скрипт делает, ключа не увидев. + +Формат TOML выбран ради **комментариев**: файл лежит в чужом репозитории, и +человек, открывший его через полгода, обязан прочитать в нём, что означает +число. JSON комментариев не знает, и объяснение приходилось держать в +документации, то есть в другом файле. + +Читается `tomllib` из стандартной библиотеки (python 3.11+), пишется руками: +писателя TOML в стандартной библиотеке нет, а комментарии переживают только +построчную правку. Поэтому версия двигается заменой одной строки, а не +перезаписью файла — иначе повышение канона стирало бы то, ради чего формат и +взят. + +Схема: + + version = 1 # версия раскладки av-dev, целое число + + [docs] + migrations = "путь/к/миграциям" # необязателен: есть БД — есть ключ + + [tasks] + dir = "tasks" # каталог задач от корня репозитория + items = "items" # имена частей каталога — необязательны + backlog = "BACKLOG.md" + roadmap = "ROADMAP.md" + +Коды выхода зовущих скриптов — общий словарь av-dev; отсюда возвращается +исключение `ConfigError`, а решает по нему вызывающий. +""" + +from __future__ import annotations + +import re +import tomllib +from pathlib import Path + +# Имя файла называет владельца: раскладку ведёт плагин `av-dev`. До слияния +# плагинов файлов было два — `docs/.docs.json` (версия канона) и +# `<каталог задач>/.tasks.json` (версия формата задач), и версии двигались +# порознь, потому что плагины ставились порознь. Плагин теперь один, версия +# одна, и дом у неё в корне репозитория: настройки нужны и проекту без `docs/`, +# и проекту без каталога задач, а корень есть у обоих. +CONFIG_NAME = ".av-dev.toml" + +# Прежние дома. Читаются не для работы, а для узнавания: увидели — говорим +# «старая раскладка, нужен upgrade», и это одна строка вместо отказа, за которым +# человек идёт заводить второй файл рядом с первым. +LEGACY = ("docs/.docs.json", "docs/.pm.json") +LEGACY_TASKS = ".tasks.json" + +# Версия раскладки — одна на плагин. Журнал версий — references/changelog.md +# скилла `doc-canon`, повышает его операция `upgrade`. +VERSION = 1 + +VERSION_KEY = "version" + + +class ConfigError(Exception): + """Файл есть, но прочитать его нельзя: битый TOML или не та схема.""" + + +def find_root(start: Path | None = None) -> Path | None: + """Корень проекта: где лежит `.av-dev.toml`, иначе где лежит `.git`. + + Обе опоры нужны: до `adopt` файла ещё нет, а работать по каталогу задач уже + можно. Возвращается None, когда нет ни того, ни другого, — тогда зовущий сам + решает, отказ это или неприменимость. + """ + here = (start or Path.cwd()).resolve() + for base in (here, *here.parents): + if (base / CONFIG_NAME).is_file(): + return base + for base in (here, *here.parents): + if (base / ".git").exists(): + return base + return None + + +def read(root: Path) -> dict: + """Настройки проекта. Файла нет — пустой словарь, это не ошибка.""" + path = root / CONFIG_NAME + if not path.is_file(): + return {} + try: + data = tomllib.loads(path.read_text(encoding="utf-8")) + except tomllib.TOMLDecodeError as exc: + raise ConfigError(f"{CONFIG_NAME} не разбирается как TOML: {exc}") from exc + except OSError as exc: + raise ConfigError(f"{CONFIG_NAME} не читается: {exc}") from exc + _validate(data) + return data + + +def _validate(data: dict) -> None: + got = data.get(VERSION_KEY) + if VERSION_KEY in data and (isinstance(got, bool) or not isinstance(got, int)): + raise ConfigError( + f"{CONFIG_NAME}: ключ «{VERSION_KEY}» — версия раскладки," + f" ожидалось целое число, а не {got!r}" + ) + for name in ("docs", "tasks"): + section = data.get(name) + if section is not None and not isinstance(section, dict): + raise ConfigError( + f"{CONFIG_NAME}: секция [{name}] — ожидалась таблица настроек," + f" а не {section!r}" + ) + + +def section(cfg: dict, name: str) -> dict: + got = cfg.get(name, {}) + return got if isinstance(got, dict) else {} + + +def version(cfg: dict) -> int | None: + got = cfg.get(VERSION_KEY) + return got if isinstance(got, int) and not isinstance(got, bool) else None + + +def legacy_files(root: Path, tasks_dir: Path | None = None) -> list[str]: + """Следы прежней раскладки — то, что говорит «проект жил до слияния». + + Каталог задач передаётся отдельно: до чтения настроек его путь неизвестен, а + искать `.tasks.json` по всему дереву значит гадать. + """ + root = root.resolve() + found = [rel for rel in LEGACY if (root / rel).is_file()] + for base in filter(None, (tasks_dir, root / "tasks", root / "docs" / "tasks")): + # Каталог задач приходит и относительным — таким его печатают в + # сообщениях; для сравнения с корнем он обязан быть абсолютным. + path = (base if base.is_absolute() else Path.cwd() / base) / LEGACY_TASKS + if not path.is_file(): + continue + path = path.resolve() + rel = path.relative_to(root).as_posix() if path.is_relative_to(root) else str(path) + if rel not in found: + found.append(rel) + return found + + +def set_version(root: Path, number: int) -> None: + """Двинуть версию, не тронув остального: правится одна строка. + + Перезапись файла целиком стёрла бы комментарии — то единственное, ради чего + формат и выбран. Ключа нет вовсе — строка встаёт первой, до всякой секции: + ключ верхнего уровня, попавший под `[docs]`, читался бы как её настройка. + """ + path = root / CONFIG_NAME + text = path.read_text(encoding="utf-8") if path.is_file() else "" + pattern = re.compile(rf"(?m)^(\s*{VERSION_KEY}\s*=\s*)(\d+)(.*)$") + if pattern.search(text): + text = pattern.sub(rf"\g<1>{number}\g<3>", text, count=1) + else: + text = f"{VERSION_KEY} = {number}\n" + text + path.write_text(text, encoding="utf-8") + + +def merge_section(root: Path, name: str, values: dict) -> list[str]: + """Дописать ключи в секцию, не тронув остального. Возвращает дописанное. + + Правка построчная по той же причине, что и у версии: перезапись файла + целиком стёрла бы комментарии. Ключ, который в секции уже есть, не трогается + вовсе — файл в чужом репозитории правит человек, и затирать его значение + своим умолчанием нельзя. + """ + path = root / CONFIG_NAME + lines = path.read_text(encoding="utf-8").splitlines() if path.is_file() else [] + header = f"[{name}]" + start = next((i for i, ln in enumerate(lines) if ln.strip() == header), None) + if start is None: + added = [f'{k} = "{v}"' for k, v in values.items()] + if not added: + return [] + block = ([""] if lines and lines[-1].strip() else []) + [header, *added] + path.write_text("\n".join([*lines, *block]) + "\n", encoding="utf-8") + return list(values) + end = next((i for i in range(start + 1, len(lines)) + if lines[i].lstrip().startswith("[")), len(lines)) + body = lines[start + 1:end] + have = {ln.split("=", 1)[0].strip() for ln in body if "=" in ln and not + ln.lstrip().startswith("#")} + added = [k for k in values if k not in have] + if not added: + return [] + insert = [f'{k} = "{values[k]}"' for k in added] + while body and not body[-1].strip(): + body.pop() + lines[start + 1:end] = [*body, *insert] + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + return added + + +def skeleton(number: int, docs: dict | None = None, tasks: dict | None = None) -> str: + """Свежий файл с комментариями — тем, ради чего взят TOML. + + Пустая секция пишется всё равно: строка «ключа нет, потому что БД нет» + читается как решение, а её отсутствие — как недосмотр. + """ + docs, tasks = docs or {}, tasks or {} + out = [ + "# Раскладка av-dev в этом проекте: версия и настройки проверок.", + "# Файл ведут скиллы плагина, править руками можно — комментарии свои.", + "", + f"{VERSION_KEY} = {number}" + " # версия раскладки; обратной совместимости нет, есть «приведён» и «нет»", + "", + "[docs]", + ] + if docs.get("migrations"): + out += [ + "# каталог миграций: по нему docs.py сверяет схему с database.md", + f'migrations = "{docs["migrations"]}"', + ] + else: + out += ["# migrations = \"путь/к/миграциям\" — появится, когда появится БД"] + out += ["", "[tasks]", + "# каталог задач от корня репозитория; имена частей — умолчания скрипта", + f'dir = "{tasks.get("dir", "tasks")}"'] + for key in ("items", "backlog", "roadmap"): + if tasks.get(key): + out.append(f'{key} = "{tasks[key]}"') + return "\n".join(out) + "\n" diff --git a/av-dev/skills/code-review/SKILL.md b/av-dev/skills/code-review/SKILL.md index 575a0f6..537d700 100644 --- a/av-dev/skills/code-review/SKILL.md +++ b/av-dev/skills/code-review/SKILL.md @@ -124,7 +124,7 @@ description: "Конвейер ревью изменения, устроенны |---|---|---| | **тема** | заводит направление проверки и требует исполнителя | `conventions.*`, `security.*`, `architecture.*`, свои документы проекта | | **источник темы** | читает как материал чужой темы, своей не порождает | `passport.*`, `database.*`, `CLAUDE.md`/`AGENTS.md`, `openspec/specs/` | -| **процессный документ** | не судит по нему изменение | `tasks/`, `docs/review.*`, `docs/adr.*`, `docs/research.*`, `docs/.docs.json` | +| **процессный документ** | не судит по нему изменение | `tasks/`, `docs/review.*`, `docs/adr.*`, `docs/research.*`, `.av-dev.toml` | **Одна процессная запись всё же читается — `docs/review.*`.** В ней лежит настройка самого конвейера: вопросы по темам, журнал дефектов, типовые узлы, diff --git a/av-dev/skills/doc-canon/SKILL.md b/av-dev/skills/doc-canon/SKILL.md index 319a46e..788f6d2 100644 --- a/av-dev/skills/doc-canon/SKILL.md +++ b/av-dev/skills/doc-canon/SKILL.md @@ -27,7 +27,8 @@ description: Привести проект к канону документов записок разведки, и дом у них общий — `shared/language.md` в репозитории плагинов, а этот файл его копия. Вычитывают их два прохода по охвату: документы — `doc-wording`, записи каталога задач — `task-wording`. -- [references/changelog.md](references/changelog.md) — журнал версий канона. +- [references/changelog.md](references/changelog.md) — журнал версий раскладки; + закрытые журналы до слияния плагинов лежат рядом. ## Три правила, из которых всё следует @@ -184,7 +185,8 @@ capability), `openspec/config.yaml`. Порядок важен — он минимизирует окно, в котором ссылки битые: -1. `docs/.docs.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть; +1. `.av-dev.toml` в корне: `version = <текущая версия>` и путь миграций в + `[docs]`, если БД есть; 2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**: незаполненное — одной честной информативной строкой, а не «TBD»; 3. **OpenSpec, если его нет или `config.yaml` остался примером** — **вызови @@ -209,10 +211,10 @@ capability), `openspec/config.yaml`. **Шагов в гейте три, и они независимы.** `docs.py check` не тянет за собой ни задачи, ни конвейер: без своих строк дрейф каталога задач и формы - `openspec/config.yaml` перестаёт ловиться совсем. Ставь соседские шаги по - следу присутствия — `<каталог задач>/.tasks.json` есть, значит ставится + `openspec/config.yaml` перестаёт ловиться совсем. Ставь соседние шаги по + следу присутствия — каталог задач с индексом на месте, значит ставится `tasks.py check --dir <каталог задач>`; `openspec/config.yaml` есть, значит - ставится `openspec.py check`. Следа нет — плагина в проекте нет, шаг не + ставится `openspec.py check`. Следа нет — этой части в проекте нет, шаг не ставится, и это **строка доклада**, а не поломка: назови, чего теперь не проверяет никто. У каждого шага своя переменная пути с тем же умолчанием на канонический путь маркетплейса; `$CLAUDE_PLUGIN_ROOT` в гейт не подставляй — @@ -273,7 +275,8 @@ capability), `openspec/config.yaml`. 3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии проекта до текущей и делай названное в каждой записи. Записи независимы и применяются по порядку. -4. Подними `canon` в `docs/.docs.json` до текущей. +4. Подними `version` в `.av-dev.toml` до текущей — правь **строку**, а не + переписывай файл: комментарии в нём принадлежат проекту. 5. `docs.py check`. 6. **Позови судей** — Skill `av-dev:doc-healthcheck`. 7. **Позови вычитку** — агент `doc-wording`, но **только по тем документам, @@ -284,17 +287,16 @@ capability), `openspec/config.yaml`. Записи журнала описывают **что сделать проекту**. Если запись этого не говорит — это дефект журнала, и о нём надо сказать, а не догадываться. -**Каталог задач повышается своим журналом, а не этим.** У него своя версия -формата — ключ `tasks` в `<каталог задач>/.tasks.json`, — и двигает её плагин -`av-dev-tasks`. Запись канона вправе сказать «позови соседа», но не вправе -двигать чужое число: две версии, которые ходят по одному журналу, разъезжаются -на первом же проекте, поставившем один плагин без другого. Отстал каталог -задач — это скажет `tasks.py check` своей строкой гейта, а повысит скилл -`av-dev:task-track`. +**Каталог задач повышается этим же журналом.** Версия одна на всю раскладку — +`version` в `.av-dev.toml`, — и записи журнала говорят про обе половины: и про +документы, и про каталог задач. Порознь версии жили, пока плагинов было три и +проект мог взять одну половину без другой; с одним плагином два числа означали +бы только вопрос, по какому журналу повышать. Что каталог задач отстал, скажет +`tasks.py check` своей строкой гейта — той же версией, что и `docs.py`. -**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.docs.json` с -версией скрипта — и только его. Применена ли запись журнала **по существу**, он -не знает: проект несёт `"canon": 6` и может не иметь того, чего требовала любая +**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.av-dev.toml` +с версией скрипта — и только его. Применена ли запись журнала **по существу**, +он не знает: проект несёт `version = 6` и может не иметь того, чего требовала любая из пройденных версий. Записи применяются руками (переименовать секцию, проставить типы, дописать раздел каждому `fix`), а ручной проход по нескольким записям подряд — ровно то место, где половина шага делается и забывается. Судьи и есть diff --git a/av-dev/skills/doc-canon/references/canon.md b/av-dev/skills/doc-canon/references/canon.md index 665c377..f49ee2d 100644 --- a/av-dev/skills/doc-canon/references/canon.md +++ b/av-dev/skills/doc-canon/references/canon.md @@ -45,8 +45,9 @@ CLAUDE.md памятка агенту: что это, стек, инварианты с severity, команды, семантика гейта, запреты AGENTS.md необязателен, лежит рядом; читается теми же +.av-dev.toml версия раскладки и настройки проверок; лежит + в корне, потому что нужен и без docs/ docs/ - .docs.json версия канона и пути, нужные проверкам passport.md | passport/ зачем и для кого; чем НЕ является; сценарии architecture.md | architecture/ как сложено — обзор; окружение и эксплуатация database.md | database/ схема хранилища; представление данных и настройки @@ -100,7 +101,7 @@ openspec/ | `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) | | `adr.*` | процессный | — | | `research.*` | процессный | — | -| `.docs.json` | процессный | — (служебный файл, не документ) | +| `.av-dev.toml` | процессный | — (служебный файл, не документ) | **Список тем открытый, и это не послабление, а механизм.** Категории `источник` и `процессный` **закрыты** — они перечислены здесь поимённо и @@ -343,10 +344,10 @@ kebab-case.** Причина не эстетическая: имя файла с ### `tasks/` -**Каталог задач канону не принадлежит.** Его ведёт отдельный плагин -`av-dev-tasks` — своим скриптом, своим конфигом `<каталог>/.tasks.json`, своей -версией формата в нём же и своим журналом версий. Канон о том числе не -высказывается и его не двигает: повышает каталог задач тот, кто его ведёт. +**Каталог задач канону не принадлежит.** Его ведёт скилл `task-track` — своим +скриптом и своими проверками; где каталог лежит и как названы его части, говорит +секция `[tasks]` в `.av-dev.toml`. Версия там одна на всю раскладку: канон и +каталог задач двигаются вместе, потому что двигает их один плагин. Канон **резервирует место** в `docs/` и внутрь не смотрит: `docs.py` каталог не открывает, его отсутствия не считает дрейфом и согласованность задач не проверяет. Проект, поставивший только канон документов, задач не ведёт @@ -538,39 +539,41 @@ kebab-case.** Причина не эстетическая: имя файла с архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт правдоподобную труху вместо находок. -## `docs/.docs.json` +## `.av-dev.toml` -```json -{ - "canon": <текущая версия>, - "migrations": "internal/store/migrations" -} +```toml +# Раскладка av-dev в этом проекте: версия и настройки проверок. + +version = 1 # версия раскладки + +[docs] +migrations = "internal/store/migrations" # если БД есть + +[tasks] +dir = "tasks" # каталог задач от корня репозитория ``` -`canon` — версия канона, под которую проект приведён, целым числом: обратной -совместимости у канона нет, есть «приведён» и «не приведён». Число подставляет +`version` — версия раскладки, под которую проект приведён, целым числом: +обратной совместимости нет, есть «приведён» и «не приведён». Число подставляет `init`, `adopt` или `upgrade`, и берётся оно из `docs.py version`, а не из -образца: литерал в образце протухает на первом же повышении канона. -`migrations` — путь каталога миграций, если БД есть; по нему `docs.py` делает -сверку с `database.md`. +образца: литерал в образце протухает на первом же повышении. +`[docs] migrations` — путь каталога миграций, если БД есть; по нему `docs.py` +делает сверку с `database.md`. `[tasks]` — где лежит каталог задач и как названы +его части; состав ключей описывает скилл `task-track`. -**Имя файла — имя плагина, который его завёл.** Канон документов ведёт -`av-dev-docs`, поэтому `.docs.json`; у каталога задач по тому же правилу -`.tasks.json`, у конвейера — `openspec/config.yaml`. До версии 13 файл звался -`.pm.json` — по плагину `av-dev-pm`, который распался на четыре и которого -больше нет; имя пережило владельца и указывало в пустоту. Прежнее имя `docs.py` -не читает: два дома для одной версии канона расходятся молча, а переименование -стоит одну команду (версия 13 журнала, и `check` называет её сам, когда видит -старый файл). +**Формат TOML взят ради комментариев.** Файл лежит в репозитории проекта, и +человек, открывший его через полгода, обязан прочитать в нём, что означает +число. JSON комментариев не знает, и объяснение приходилось держать в другом +файле. Отсюда же правило записи: скрипты правят **строку**, а не переписывают +файл — перезапись стёрла бы то, ради чего формат и выбран. -**Ключа `tasks` здесь больше нет.** Настройки каталога задач вернулись в свой -файл `<каталог задач>/.tasks.json`, потому что ведёт их другой плагин: конфиг, -лежащий в `docs/`, был бы домом, которого нет у проекта, поставившего учёт работ -без канона документов. Состав ключей описывает тот плагин, а не канон. Там же — -**версия формата задач**, и она своя: у проекта без `docs/` версии канона нет -вовсе, сверять её было бы не с чем. Прежний ключ читается, пока живы -непереехавшие проекты, и `tasks.py` говорит о нём замечанием на каждом -прогоне — версия 8 журнала просит его убрать. +**Файл один, и лежит он в корне.** До слияния плагинов их было два — +`docs/.docs.json` с версией канона и `<каталог задач>/.tasks.json` с версией +формата задач, — и версии двигались порознь, потому что плагины ставились +порознь. Плагин теперь один, версия одна, а корень выбран потому, что он есть и +у проекта без `docs/`, и у проекта без каталога задач. Прежние имена не +читаются: два дома для одной версии расходятся молча. Увидев их, `docs.py` и +`tasks.py` говорят «прежняя раскладка» и зовут `upgrade` — версия 1 журнала. Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py` игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом diff --git a/av-dev/skills/doc-canon/references/changelog-before-merge.md b/av-dev/skills/doc-canon/references/changelog-before-merge.md new file mode 100644 index 0000000..1c60a8d --- /dev/null +++ b/av-dev/skills/doc-canon/references/changelog-before-merge.md @@ -0,0 +1,784 @@ +# Журнал версий канона до слияния плагинов + +**Журнал закрыт.** Он описывает версии канона документов 1–14 — время, когда +плагинов было три и у канона была своя нумерация. Действующий журнал — +[changelog.md](changelog.md), и его версия 1 идёт **после** записи 14 отсюда. + +Записи не переписаны под нынешние имена: адрес и имя скилла, верные на день +записи, остаются там как свидетельство. Записи ниже версии 13 зовут служебный +файл `docs/.pm.json` — так и было; переименование делает запись 13, а переезд в +`.av-dev.toml` — запись 1 действующего журнала. + +Проект, отставший от канона 14, идёт сперва по этим записям снизу вверх от своей +версии до 14, и только потом переходит в действующий журнал. + +--- + +## Версия 14 — 2026-08-11 + +У ADR стало два законных источника. Прежде запись цитировала только архивный +`design.md`, то есть решение, принятое по ходу изменения. Решение, принятое +**разведкой** — намеренный отказ, выбор подхода, «проверили и не делаем», — не +имеет `design.md` по построению: change по нему не заводится никогда. Триггер +канона такое решение ловит («намеренный отказ от очевидного подхода»), а дома у +него не было, и оно оседало в записке разведки или в переписке. + +**Что изменилось.** `adr/` принимает второй источник — записку разведки. Правило +«промоут, а не второе сочинение» не тронуто: запись по-прежнему цитирует уже +написанное и **называет источник**, изменилось только то, что источников два. +Следом сказали то же самое: карта домов, разрез проверки `doc-consistency`, вход +и устав самого агента, скелеты `docs/adr/README.md` и `docs/adr/template.md`. + +**Почему это версия, а не правка текста.** Два следствия уезжают в репозиторий +проекта. По карте домов судит агент согласованности — прежняя редакция читала ADR +со ссылкой на записку разведки как нарушение; а скелеты `adr/` лежат в проекте +файлами и говорят там от имени канона. + +**Что сделать проекту.** + +1. Ничего с существующими записями: прежние ADR ссылаются на `design.md`, и это + по-прежнему верно. +2. **Поднять шапку `docs/adr/README.md`**: «промоут поверх архивного `design.md`» + → «промоут поверх уже написанного», с обоими источниками. Точный текст — в + [skeletons.md](skeletons.md), раздел `docs/adr/README.md`. +3. **Поднять `docs/adr/template.md`**: строка `- **Источник:**` называет два + возможных источника. +4. `docs/.docs.json`: `"canon": 14`. + +**Чего делать не надо.** Заводить ADR задним числом по старым разведкам. Запись +заводится, когда решение принимается, а не когда о нём вспомнили: сочинённое +через полгода обоснование — ровно то «второе сочинение», против которого правило +и написано. + +--- + +## Версия 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 + +Спринты отменены. Работа идёт задача за задачей, и замороженный набор перестал +что-либо удерживать: он отвечал на вопрос «что делать дальше», а между наборами +на этот вопрос не отвечал никто. + +**Что изменилось.** Индексов задач два вместо трёх: `SPRINT.md` упразднён. +Приоритет стал тем, чем он и является, — **порядком строк в `BACKLOG.md`**: +первая строка секции это то, что делают следующим. Назначает порядок человек, +машина его не выводит; двигают его `move --after` и `move --first` с причиной. + +Гейт готовности записи стоял на взятии задачи в спринт — единственном месте, где +её судили целиком. Момент нужен и без спринта: теперь это команда +`tasks.py ready <слаг>`, и зовёт её тот, кто берёт задачу в работу. + +Ритуал между спринтами (`av-dev-tasks:session`) стал скиллом груминга +(`av-dev-tasks:groom`): два вопроса — что сейчас самое важное и что перестало +быть важным. + +**Что сделать проекту.** + +1. **Вернуть задачи из набора в беклог и снести `SPRINT.md`.** Порядок такой: + `git rm tasks/SPRINT.md`, затем `tasks.py check --dir tasks --fix`. Строки + набора после удаления файла становятся бездомными, и `--fix` возвращает их в + беклог **в конец своей секции** — с пометкой, что позицию назначает человек. + Наоборот делать нельзя: `check` без удалённого файла увидит третий индекс и + станет ругаться на него, а не чинить. +2. **Снять теги `sprint:<слаг>`** с записей — `tasks.py edit <слаг> --rm-tag + sprint:<слаг>`. Тег больше никем не читается, а `check` о нём молчит: он + законный свободный тег. Пропущенный вреда не сделает, но и пользы не несёт. +3. **Расставить порядок** — первый груминг: `av-dev-tasks:groom`. После шага 1 + очередь состоит из того, что машина поставила в конец, то есть очереди нет + вовсе. Пока порядок не назначен, «что делать дальше» по-прежнему без ответа. +4. Поправить упоминания спринта в `CLAUDE.md` проекта, если они были: слот + «общий станок» переехал в груминг под именем «что считается сломанным», + ориентир «5–8 задач в спринте» стал ориентиром размера порции разбора. +5. `docs/.pm.json`: `"canon": 12`. + +**Чего делать не надо.** `REJECTED.md`, `ROADMAP.md` и файлы `items/` не +меняются: спринт жил только в собственном индексе и в тегах. + +--- + +## Версия 11 — 2026-08-09 + +Каталог задач уехал из `docs/` в корень репозитория. Версия 8 отпустила его из +канона — перестала требовать, перестала проверять, — но место он занимал всё то +же, `docs/tasks/`. Полдела: каталог, принадлежащий одному плагину, лежал внутри +дерева, которым владеет другой. Проекту, поставившему учёт работ без канона +документов, приходилось заводить `docs/` ради одной вложенной папки. + +**Что изменилось.** Дом задач — `tasks/` в корне репозитория. `tasks.py` ищет его +там первым; `docs/tasks/` и `doc/tasks/` остаются в списке поиска для +непереехавших проектов, а `init` заводит только в корне. Настройки — там же, +`tasks/.tasks.json`. + +**Что осталось терпимым.** `docs.py` по-прежнему не считает `docs/tasks/` файлом +вне канона: непереехавший проект не должен получать выдуманную ошибку вдобавок к +этой записи, которая и так велит ему переехать. + +**Что сделать проекту.** + +1. `git mv docs/tasks tasks` — одним коммитом вместе с шагом 2, чтобы ссылки не + жили битыми между коммитами. +2. **Починить относительные ссылки внутри записей.** Файл `tasks/items/x.md` + стал на уровень ближе к корню: `../../passport.md` в теле записи теперь + `../docs/passport.md`. Тот же сдвиг у ссылок из индексов. Это самая тихая + часть переезда: битая относительная ссылка не мешает `tasks.py check`, её + ловит только `docs.py check` и только у документов канона. +3. Проверить ссылки **на** задачи снаружи: `CLAUDE.md`, `README.md`, гейт, + `docs/review.md`. Путь `docs/tasks/...` в них теперь ведёт в никуда. +4. Поправить путь в гейте: `tasks.py check --dir tasks`. +5. `docs/.pm.json`: `"canon": 11`. + +## Версия 10 — 2026-08-09 + +Проверка формы `config.yaml` ушла к тому, кто файл заводит. Версия 9 перенесла в +конвейер настройку OpenSpec и честно назвала остаток: форма и сторож версии +остались в `docs.py`, то есть у файла было два плагина — один заводит, другой +проверяет. Остаток закрыт. + +**Что появилось.** Скрипт `openspec.py` в скилле `av-dev-code:openspec`, две +команды: `check --dir <корень>` — форма в проекте, `form` — сверка слепка с живым +OpenSpec. Коды выхода те же, что у `docs.py` и `tasks.py`. + +**Что удалено из `docs.py`.** Константы `OPENSPEC_*`, проверка формы, сторож +версии и подкоманда `openspec-form` — 252 строки. Скрипт канона про +`openspec/config.yaml` не говорит теперь ничего; `openspec/specs/` он по-прежнему +знает, потому что это дом темы `requirements` и часть карты тем. + +**Что стало лучше по дороге.** Адреса `docs/passport.md` и `CLAUDE.md` требуются +теперь **только к тем документам, которые в проекте есть**. Прежняя проверка +требовала их безусловно, то есть на проекте без канона документов требовала +битую ссылку. Теперь отсутствие документа — строка «не проверялось» с указанием, +что без канона конвейер работает вслепую. + +**Что осталось за каноном.** Один вопрос, и это не форма: не пересказан ли в +`context` документ, у которого есть свой дом. Разрез — утверждение, опровергаемое +открытием другого файла, против строки «открой такой-то файл»; машине он не +виден, судит агент `doc-consistency`, и `config.yaml` у него во входе. + +**Что сделать проекту.** + +1. Заменить в гейте и в скриптах `docs.py openspec-form` на `openspec.py form`. + Подкоманды больше нет: прежний вызов упадёт ошибкой употребления (код 2), а не + промолчит. +2. **Добавить в гейт шаг `openspec.py check`, если проект работает по OpenSpec.** + Форму раньше проверял `docs.py check` заодно; теперь он о ней молчит, и без + отдельного шага незаменённый пример в `config.yaml` перестанет ловиться. Это + главная потеря этого повышения, и она тихая. +3. Проект по OpenSpec без установленного `av-dev-code` — форму не проверяет + никто. Либо поставить плагин, либо назвать это принятым риском вслух. +4. `docs/.pm.json`: `"canon": 10`. + +## Версия 9 — 2026-08-09 + +OpenSpec уехал в конвейер. Каталог `openspec/` версией 7 был объявлен слотом +канона: `init` его заводил, `adopt` тоже, образец `config.yaml` лежал в скелетах, +а отсутствие каталога `docs.py` считал отказом. Разрез был проведён не там. По +OpenSpec работает конвейер — без каталога не запускаются ни `opsx:propose`, ни +ревью дизайна, ни сверка требований, — а канон документов о нём только +высказывался. Проект, которому конвейер не нужен, получал отказ за отсутствие +того, чем не пользуется. + +**Что появилось.** Скилл `av-dev-code:openspec`: заводит каталог, заменяет +закомментированный пример в `config.yaml` настройкой, объясняет разрез между +ссылкой и пересказом. Образец файла переехал туда же — в +`references/config-skeleton.md` того скилла. + +**Что изменилось.** `init` и `canon adopt` OpenSpec больше не заводят, а **зовут +скилл конвейера**; вызов не разрешился — плагина конвейера нет, и это строка +доклада, а не поломка. Отсутствие `openspec/` для `docs.py check` стало +неприменимостью вместо отказа: остальные четыре проверки формы идут только при +живом каталоге. + +**Что осталось на месте и почему.** Проверка формы `config.yaml` и сторож версии +(`docs.py openspec-form`) пока живут в скрипте канона — переносить их значит +заводить в конвейере свой скрипт, а этого у него нет ни одного. Разрез названного +это не отменяет, но и не завершает: **у файла сейчас два плагина — один заводит, +другой проверяет**, и это временное состояние, а не задуманное. + +**Что сделать проекту.** + +1. Ничего не переносить: файлы проекта эта версия не двигает. Меняется только то, + кто их заводит. +2. Проверить, что плагин `av-dev-code` установлен, если проект работает по + OpenSpec. Без него `docs.py check` про каталог промолчит — и молчание это + законное, так что отсутствие настройки перестанет ловиться само. +3. Проект **не** работает по OpenSpec: убедиться, что `openspec/` нет, и + перестать держать его пустым ради проверки. Она больше не требует каталога. +4. `docs/.pm.json`: `"canon": 9`. + +## Версия 8 — 2026-08-09 + +Канон отпустил каталог задач. Плагин `av-dev-pm` расколот на `av-dev-docs` +(документы) и `av-dev-tasks` (учёт работ), и каждый теперь ставится сам по себе. +Пока владелец был один, `docs/tasks/` числился слотом канона: `docs.py` требовал +каталог, звал внутрь чужой скрипт и выдавал его дрейф за свой, а настройки задач +жили ключом `tasks` в `docs/.pm.json`. Для проекта, поставившего только документы, +всё это — отказ на ровном месте: задач он не ведёт, и требовать их не за что. + +**Что изменилось.** Каталог задач канону не принадлежит; канон резервирует ему +место в `docs/` и внутрь не смотрит. `docs.py` больше не проверяет согласованность +задач вовсе — это делает `tasks.py` сам, командой своего плагина. Дом настроек +каталога задач — `<каталог задач>/.tasks.json`; ключ `tasks` в `docs/.pm.json` +читается, только пока своего файла нет, и об этом говорится замечанием. + +**Что удалено.** Проверка `check_tasks` из `docs.py` и ключ `"tasks"` из скелета +`docs/.pm.json`. + +**Что сделать проекту.** + +1. Перенести настройки задач: содержимое ключа `"tasks"` из `docs/.pm.json` — в + `docs/tasks/.tasks.json` тем же объектом. Ключа в проекте нет (имена файлов + и заголовков умолчательные) — переносить нечего, шаг пропускается. +2. Удалить ключ `"tasks"` из `docs/.pm.json` после переноса. Оставленный он не + читается, и `tasks.py` скажет об этом замечанием на каждом прогоне. +3. Проверить, что согласованность задач по-прежнему кто-то гоняет: раньше её + тянул за собой `docs.py check`, теперь — только `tasks.py check`. **Если в + гейте проекта стоял один `docs.py`, добавить туда второй шаг** — иначе дрейф + индексов перестанет ловиться молча, и это самая вероятная потеря на этом + повышении. +4. Установить оба плагина, если нужны оба: `av-dev-docs` и `av-dev-tasks` + вместо прежнего `av-dev-pm`. Прежний из `enabledPlugins` убрать. +5. `docs/.pm.json`: `"canon": 8`. + +## Версия 7 — 2026-08-07 + +`openspec/` был предпосылкой, о которой канон говорил, но за которой не следил. +Каталог назван в раскладке, `openspec/specs/` объявлен домом темы `requirements`, +`config.yaml` описан абзацем — а заводил всё это человек руками, и проверялось +из перечисленного ничего. Заведение нового проекта проходило мимо: `init` +собирал документы канона и оставлял проект без каталога, без которого не работают +ни `opsx:propose`, ни ревью дизайна, ни сверка требований. + +Хуже отсутствия оказался файл из коробки. `openspec init` кладёт `config.yaml`, +где `context` и `rules` — закомментированный пример на английском. Такой файл +читается как настроенный: он есть, он валиден, имя правильное. Работает он как +пустой, и узнаётся это по предложению, написанному на другом языке, с +capability по имени пакета и без единого `SHALL`. + +**Что изменилось:** + +1. **`init` заводит OpenSpec сам** — `openspec init --tools claude`, до первого + документа канона. Команда названа в каноне поимённо, потому что её печатает + отказ `docs.py`. +2. **У `openspec/config.yaml` появилась каноническая форма** и скелет в + `skeletons.md`. Содержание — только то, что нужно **в момент порождения + артефакта**: язык, правила именования capability, придирки валидатора и + **адреса** документов канона. Пересказ паспорта, инвариантов, конвенций и + правил ревью в него не переносится. +3. **`docs.py check` проверяет пять вещей:** каталог `openspec/` есть; файл + называется `config.yaml` (`config.yml` OpenSpec читать не станет и об этом не + сообщит); `context` и `rules.specs` не остались примером, а правила для + `specs` называют `SHALL`; `context` называет `passport` и `CLAUDE.md`; ключи + под `rules:` — имена артефактов схемы, а не опечатки. +4. **За свежестью формы следит машина, а не память.** Схема и перечень + артефактов — слепок чужого инструмента; `check` сравнивает `major.minor` + установленного OpenSpec с версией, на которой форма сверялась, и при + расхождении даёт замечание. Перепроверяет `docs.py openspec-form`, и чинится + расхождение **в плагине, а не в проекте**. +5. **Шестое проверяет агент.** Отличить ссылку на документ от пересказа документа + машина не умеет — это работа `doc-consistency`, и в таблице «Что проверяет + машина, а что человек» она стоит строкой. + +**Что переехало:** ничего в раскладке `docs/`. Ни один файл не переименовывается +и не перемещается. + +**Что сделать проекту:** + +1. Нет `openspec/` — завести: `openspec init --tools claude`. Команда кладёт ещё + и `.claude/skills/openspec-*` с `.claude/commands/opsx/*`; это её нормальная + работа, удалять их не надо. +2. Открыть `openspec/config.yaml` и привести к скелету из + [skeletons.md](skeletons.md): блок `context` с языком, правилами именования + capability, требованием `SHALL` и **адресами** `docs/passport.md` и + `CLAUDE.md`; блок `rules` с четырьмя правилами для `specs`. +3. **Вычистить из `context` пересказ.** Инварианты, перечень конвенций, состав + шагов гейта, правило выбора метки и состав проходов ревью — заменить ссылкой + на дом. Признак пересказа простой: строку можно опровергнуть, открыв другой + файл проекта. +4. Проверить имя файла: `config.yml` переименовать в `config.yaml`. Если жили оба + — содержимое `.yml` до сих пор не читалось никем, и переносить из него нужно + именно то, чего нет в `.yaml`. +5. `docs/.pm.json`: `"canon": 7`. + +--- + +## Версия 6 — 2026-08-07 + +Версия 5 объявила: **каждый документ `docs/` — тема ревью**. Правило оказалось +верным ровно наполовину и потому вредным целиком. Паспорт и схему хранилища +ревью читает, но темами они не являются — они задают границу, по которой судит +чужая тема. Журнал решений и журнал наблюдений ревью изменения не нужны вовсе: +ADR объясняет прошлое решение, а не предъявляет требование к изменению. + +Разметчик, применявший плоское правило буквально, обязан был либо завести +фантомные темы `passport`, `adr`, `database`, `research` и продублировать ими +работу тем `architecture` и `operations`, либо потерять четыре документа молча — +а молчащая потеря и есть то, против чего канон написан. + +**Что изменилось:** + +1. **Три категории документов вместо одной.** Разрез проверяемый: можно ли по + документу сказать «в этом изменении сделано не так»? **Тема** — да, прямо + (`conventions`, `security`, `architecture`, свои документы проекта). + **Источник темы** — нет, но он задаёт границу для чужой темы (`passport.*` → + `architecture`, `database.*` → `operations`, `CLAUDE.md` → `autotests`, + `openspec/specs/` → `requirements`). **Процессный документ** — нет, он про то, + как мы работаем (`tasks/`, `review.*`, `adr.*`, `research.*`, `.pm.json`). +2. **Категории `источник` и `процессный` закрыты, категория `тема` открыта.** + Прежде открытым был весь список, и «не темы ровно две» противоречило + собственной раскладке канона. Теперь пополняется только одно множество, и + документ, которого нет в раскладке, — однозначно своя тема проекта. +3. **`adr/` и `research/` уходят из входа ревью изменения.** Прогон их больше не + открывает. Проверяться они не перестали: ADR без ссылки на архивный + `design.md`, замена без парного статуса, число без провенанса — это по-прежнему + работа `doc-consistency` и `doc-code-drift`, на сессии между спринтами. +4. **`docs.py` печатает категорию в отказе.** «Нет источника passport» читается + иначе, чем «нет темы security». Обязательность при этом не изменилась: + заводятся все документы одинаково и с первого дня. +5. **У задачи появилась метка — `small`, `medium` или `large`.** Это итог + классификации и **единственный вход, по которому конвейер выбирает + исполнителей** на обеих стадиях ревью. Прежние имена `quick`, `standard` и + `wide` описывали глубину прогона, то есть свойство ревью; метка описывает + **задачу** — а выбирают по ней одно и то же. Слово «ступень» уходит: + у одной вещи одно имя. +6. **Метка выводится из двух осей и не равна ни одной из них.** Размер (малое, + среднее, крупное) и сложность (знакомое, незнакомое); метка — максимум по + ним. Малое **незнакомое** изменение получает `large`, трогая один узел, — + поэтому размер и метка пишутся отдельными строками, и выводить одно из + другого нельзя. + +**Цена, записанная явно:** расхождение изменения с записанным решением прогоном +больше не ловится. Раньше архитектурный проход мог сказать «здесь отменено +решение ADR-2026-03-11, парного статуса нет»; теперь это скажет только сверка +документации. Сделка сознательная: чтение всего каталога решений оплачивалось на +каждой задаче, а срабатывало на единицах. + +**Что переехало:** ничего в раскладке. Ни один файл не переименовывается и не +перемещается. + +**Что сделать проекту:** + +1. `docs/review.*`, подраздел «Вопросы по темам»: убрать вопросы, адресованные + `passport`, `database`, `adr`, `research` и `review` — **ни одно из этих имён + больше не тема**. Под каноном 5 темой был каждый документ `docs/`, поэтому + такие вопросы там законны и почти наверняка есть. Переадресовать: + про границу домена и про решение → `architecture`; про хранилище, настройку и + измеренное число → `operations`. Вопрос, который никуда не переадресовывается, + удалить, а не оставить висеть: адресованный несуществующей теме, он не + задаётся никем и молча. +2. Там же, «Недоступно проверке»: те же пять имён убрать из разнесения по темам, + переразнеся содержимое по оставшимся. +3. Там же: подраздел **«Триггеры профиля» → «Триггеры метки»**, и разнести его + на **три** списка вместо двух — «крупное здесь» (про объём), «незнакомое + здесь» (про форму решения) и «мелкое здесь» (опускает до `small`). Раньше + первые две оси были склеены в один список, и потому объём в правило по факту + не входил. +4. **Переименовать метки прогона везде, где проект их называет** — в «Триггерах + метки», в «Недоступно проверке», в журнале дефектов: `quick` → **`small`**, + `standard` → **`medium`**, `wide` → **`large`**. Метка это итог классификации + задачи, и три её значения — часть общего словаря канона и конвейера. Слово + «ступень» из документов уходит: у одной вещи одно имя. +5. Проверить, что свои темы проекта не совпадают именем с закрытыми категориями: + `docs/passport/`, `docs/adr/`, `docs/research/`, `docs/database/`, + `docs/review/` — это слоты канона, а не свои темы, и своим смыслом их + наполнять нельзя. +6. Ничего не заводить и не удалять: раскладка канона 6 совпадает с раскладкой + канона 5 файл в файл. +7. `docs/.pm.json`: `"canon": 6`. + +--- + +## Версия 5 — 2026-08-06 + +Канон перестал быть списком файлов и стал **списком тем ревью**. Раскладка та же, +но читается иначе: документ в `docs/` — это направление проверки, а не просто +текст. Отсюда три правки, и все три развязывают то, что раньше было жёстко +сцеплено. + +**Что изменилось:** + +1. **Тема живёт файлом или каталогом, на выбор проекта.** `docs/security.md` и + `docs/security/` — одно и то же; тема разрослась, стала каталогом с + `README.md` — канон не сменился и версия не двинулась. Прежде форма была + задана поимённо: `conventions`, `research` и `adr` обязаны были быть + каталогами, остальные — файлами, и обосновать это было нечем. Обе формы сразу + — ошибка: два дома для одного факта расходятся молча. +2. **Список тем открытый.** Всё, что проект кладёт в `docs/`, становится темой + ревью и попадает в план каждого прогона; именной оптики у такой темы нет, её + разбирает общий проход конвейера, заведённый ровно за этим. + Прежде `docs.py` называл незнакомый файл «вне канона» — теперь называет своей + темой проекта и перечисляет их в отчёте. Не темы ровно две: `docs/tasks/` и + `docs/review.*`. +3. **`AGENTS.md` рядом с `CLAUDE.md` — законно.** Он почти стандарт; обязателен + по-прежнему только `CLAUDE.md`, но если лежат оба, читаются оба, и проверки + канона смотрят на второй так же, как на первый. + +**Что переехало:** + +- в `docs/review.*`: **«Вопросы к проходам» → «Вопросы по темам»**, форма + `<тема>: <вопрос> (<провенанс>)`. Причина не косметическая: вопрос, + адресованный проходу, перестал задаваться молча в тот день, когда тот уехал в + верхнюю ступень ревью. Тема переезд прохода переживает, имя прохода — нет; +- там же **«Недоступно проверке» — по темам**, оба подраздела. + +**Что сделать проекту:** + +1. Ничего не переименовывать, если всё уже разложено по канону 4: обе формы + дома законны, и текущая — одна из них. +2. `docs/review.*`, подраздел «Вопросы к проходам»: переименовать в «Вопросы по + темам» и переадресовать каждый вопрос теме вместо имени прохода. Темы ядра — + `requirements`, `autotests`, `conventions`, `architecture`, `security`, + `operations`. +3. Там же «Недоступно проверке»: разнести обе половины по темам. +4. Проверить, не лежит ли в `docs/` документ, который раньше считался лишним и + потому не заводился. Теперь он законен и станет темой ревью — это и есть + способ добавить проверку, которой в конвейере нет. +5. `docs/.pm.json`: `"canon": 5`. +6. Позвать судей `doc-consistency` и `doc-code-drift` — шагом 6 `upgrade`. + +## Версия 4 — 2026-08-05 + +Две правки, обе про то, как читается каталог задач. Первая — секция роадмапа +переименована, и вместе с именем расширен её смысл; достигнутое переехало вниз. +Вторая — **у каждой записи появился тип, и тип определяет, что с записью можно +делать**. Раскладка не меняется, файлов канона не прибавляется. + +**Что переехало:** + +- секция роадмапа `Разработка` → **`Сопровождение`** (англ. `Tooling` → + **`Operations`**). Прежнее имя называло слишком много: роадмап **весь** про + разработку, и секция с таким именем не отличалась от остальных ничем; +- **тип записи** — из префикса заголовка (`[goal]`/`[idea]`) и тега + `kind:<род>` в **поле меты `Тип`** первой строкой. Эмодзи в заголовке от него + производна; +- **поле места** у задачи: `Секция` → **`Категория`**. У цели остаётся `Секция`: + у задачи поле называет полку домена, в которую она вернётся из спринта, у цели + — часть роадмапа, то есть состояние очереди. Одно имя на два смысла их и + смешивало. + +**Что добавилось:** + +1. **Смысл секции расширен.** Было «инструмент и процесс», стало «чем держат + проект: инструмент, процесс, эксплуатация». Метрики, логи, инфраструктура, + выкладка и дежурство — сюда же. Расширение не косметическое: английское + `Operations` при узком смысле обещало бы эксплуатацию, а внутри лежал бы + линтер. +2. **Общий словарь трёх мест** — [canon.md](canon.md), раздел «Сопровождение и + эксплуатация». Сопровождение — всё, чем держат проект; эксплуатация — его + часть, работа системы на проде. `ROADMAP.md`, секция `Сопровождение` — план + работ; `architecture.md`, раздел «Эксплуатация» — как устроено сейчас; + эксплуатационный проход ревью — оптика проверки. Слить их в одно слово + нельзя: они отвечают на разные вопросы. Слово **«поддержка» не употребляется + вовсе** — в нём слышится помощь пользователю. +3. **Граница с возможностями проходит по тому, кто наблюдает.** «Приложение + сообщает о своём состоянии» — возможность приложения, её место среди прочих + целей. «Дежурный видит состояние на одном экране» — сопровождение. Одни и те + же метрики попадают в разные секции роадмапа, и это верно. +4. **Порядок секций стал каноническим**, и `Готово` переехало **вниз**: + `Запланировано` | `Направления` | `Сопровождение` | `Готово`. Достигнутое + копится — через год этой секции больше, чем всех остальных вместе, — и стоя + первой она отодвигает за экран то, ради чего роадмап открывают чаще всего. + Порядок проверяет `tasks.py check`, переставляет `check --fix`. +5. **Заголовок секции отбивается пустой строкой с обеих сторон.** Прежде + проверялась только строка после заголовка; перестановка секций двигает целые + блоки, и два заголовка оказываются вплотную. Правит `check --fix`. +6. **Тип — единственная ось записи, закрытый словарь из пяти значений:** + `goal` | `feature` | `fix` | `chore` | `research`. Осей было две — тип записи + (`goal`/`idea`/`task`) и род работы (`kind:` тегом), — но из двенадцати + клеток произведения законны были шесть, а алгоритм работы крепится к роду, а + не к типу. Оси схлопнуты. +7. **Тип задаёт схему тела:** какие разделы обязательны, какие допустимы, нужна + ли цель, берётся ли запись в спринт. Проверяет `sprint take`, замечания даёт + `check`. Два раздела новые: **`Воспроизведение`** у `fix` (не + воспроизводится — это `research`, а не `fix`; правило было записано и не + проверялось) и **`Вопрос` + `Куда ляжет ответ`** у `research` вместо + критериев приёмки (приёмка разведки — записанный ответ, и критерии в форме + «оракул: тест» ей натянуты). +8. **Тип `idea` упразднён.** Он значил не род работы, а состояние + незаполненности, а состояние типом быть не может. Теперь оно называется + честно: `research` без раздела «Вопрос» — **сырьё**. В спринт не берётся, как + и прежняя идея, лежит **в конце своей категории** (проверяет `check`, + переставляет `--fix`) и отбирается `list --raw`. Порядка «по важности» в + беклоге по-прежнему нет: этот порядок производен от типа, а не назначен + человеком. +9. **Алгоритм работы над каждым типом** — отдельным файлом, + `skills/tasks/references/task-<тип>.md`: схема, что проверяет машина, что + человек, и порядок шагов. +10. **Имена файлов проверяются.** Правило «текст русский, имена английские» + стояло в каноне и не было подкреплено ничем: `docs.py` имён не смотрел вовсе. + Теперь смотрит — кириллица и не-kebab-case **жёстко**, форма имени + `ADR-ГГГГ-ММ-ДД-slug.md` жёстко, транслит **эвристикой**, то есть + замечанием. Заодно из раскладки канона убраны плейсхолдеры `<тема>.md`, + приглашавшие называть файлы по-русски. +11. **Два агента вместо обещания.** В каноне была таблица «Что проверяет машина, + а что человек», и её правая колонка три версии описывала судью, которого не + существовало. Судьи заведены и разведены по глубине: **`doc-consistency`** + (документ ↔ документ ↔ openspec: факт в двух домах, прямое противоречие, + поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса, + число без провенанса, заглушка вместо честной строки); **`doc-code-drift`** + (документ ↔ код по закрытому перечню фактов). Оба зовутся раз в спринт на + сессии, а также после adopt и после upgrade, на весь канон разом. + +**Что сделать проекту:** + +1. Переименовать заголовок секции в `docs/tasks/ROADMAP.md`: `## Разработка` → + `## Сопровождение` (или `## Tooling` → `## Operations`, если индекс + английский). **`check --fix` этого не сделает**: регистр канонической секции + он правит сам, а чужую секцию только называет ошибкой — смысл за человеком. +2. Поправить поле `- **Секция:**` в файлах целей, которые в ней лежат. Порядок + именно такой: сперва заголовок, потом `python3 tasks.py check --dir + docs/tasks` покажет расхождение поимённо. +3. Перечитать состав секции: цели про выкладку, метрики, логи и инфраструктуру, + если они лежали в `Направлениях` за неимением места, переезжают сюда. +4. Прогнать `python3 tasks.py check --dir docs/tasks --fix`. За один проход он + переставит секции роадмапа в канонический порядок (`Готово` уедет вниз вместе + со всем содержимым), поправит отбивку заголовков и **переведёт записи на + типы**: перенесёт значение из тега `kind:` и префикса `[goal]`/`[idea]` в поле + `Тип`, снимет тег, поставит эмодзи в заголовок, переименует `Секция` → + `Категория` у задач и снесёт сырьё в конец категорий. +5. Разобрать то, что `--fix` вернул пометкой `НЕОДНОЗНАЧНО`. Главный случай — + **записи без типа**: заведённые до появления рода работы, они не несут ни + тега, ни префикса, и машина их не угадывает (`feature` от `chore` не + отличает). Проставить руками: `edit <слаг> --type …`. +6. Дописать новые обязательные разделы у задач, которые собираются в спринт: + `Воспроизведение` у каждого `fix`, `Вопрос` и `Куда ляжет ответ` у каждого + `research`. Не «заодно по всему беклогу», а порциями переоценки: `check` + ошибкой это не считает, отказывает только `sprint take`. Сколько задач готово + к взятию, печатает блок здоровья `check`. +7. Прогнать `python3 docs.py check`: он назовёт имена файлов не по правилу. + Кириллицу и не-kebab-case править обязательно, транслит — по решению + человека. **Переименование ADR это перенос ссылок**: слаг стоит в + `adr/README.md`, в `architecture.md` и в чужих документах, и делается одним + проходом, иначе останутся битые ссылки (их `docs.py` потом и покажет). +8. `docs/review.md`, подраздел «Триггеры профиля» — переписать целиком, он + отстал дважды. Снести перечень мест для `deep`: профиль упразднён вместе с + проходом независимой реализации, и перечень стал указателем в пустоту. + Оставшийся перечень перевести на новое правило: `wide` теперь означает не + «новое понятие», а **крупное или незнакомое** изменение и рассчитан на 5–10% + задач; отдельным списком назвать, что здесь считается **мелким** (это `quick`). + Форма подраздела — в [skeletons.md](skeletons.md). Там же проверить журнал + дефектов и «Недоступно проверке» на упоминания независимой реализации: класс + «форма решения, где спека выбора не сделала» переезжает в подраздел «перестали + проверять сознательно», а рядом с ним встаёт вторая честная строка — на + `quick` и `standard` не проверяется ничего, что требует запуска. +9. `docs/.pm.json`: `"canon": 4`. +10. Позвать **обоих судей** — `doc-consistency` и `doc-code-drift`, шагом 6 + `upgrade`. Пунктов выше десять, половина из них ручная, и именно здесь видно, + какие сделаны только наполовину: переименования секций и полей разводят + документы, а `check` сверяет число версии, а не существо. Первый прогон на + живом проекте вдобавок самый урожайный — правило единственного дома до сих пор + никто не проверял. Разбирать порциями, а не одним заходом. + +## Версия 3 — 2026-08-04 + +Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность +приложения, задача — шаг к ней, достигнутое из роадмапа не исчезает. Плюс род +работы, раздел «Затрагивает» и новое умолчание профиля ревью. Раскладка меняется +в одном файле, но переименование и смена секций тянут за собой ссылки, поэтому +шаги делаются одним заходом. + +**Что добавилось:** + +1. **Род работы** — тег `kind:<род>` в мете задачи, словарь закрыт: + `feature` | `fix` | `chore` | `research`. Обязателен у задачи, у цели + запрещён. `sprint take` без него отказывает, `check` о пропаже напоминает + замечанием. Определение — [canon.md](canon.md), раздел `tasks/`; смысл и + причина, почему тегом, — в SKILL.md скилла `tasks`, раздел «Род работы». +2. **Раздел «Затрагивает»** в теле задачи — перечень границ, которых изменение + касается (эндпоинт, таблица и миграция, формат на диске, публичный тип). Как + и критерии приёмки, требуется к взятию в спринт, а не к заведению. +3. **Секции роадмапа** — четыре вместо двух и **канонические**, в отличие от + секций беклога: `Готово` (достигнутые цели строкой с датой, без ссылки на + файл), `Запланировано` (очередь значима), `Направления` (очереди нет), + `Разработка` (инструмент и процесс, не возможности приложения). Английский + вариант — `Done` | `Planned` | `Directions` | `Tooling`, один язык на весь + индекс. Переименованию проектом не подлежат: у каждой свой смысл, и в первую + пишет сам `close`; `tasks.py check` проверяет состав. +4. **Форма заголовка записи** — по типу: задача отвечает на «что нужно сделать» + и пишется глаголом в неопределённой форме («Не отбрасывать молча лишние + символы»), цель — на «что приложение будет уметь», идея просто называет, о + чём она. `check` считает заголовки не в форме действия и печатает число в + блоке здоровья. Годность формулировки — не машине: её смотрит новый агент + `task-form` (форма записи, только чтение), а язык текста — `doc-wording`. +5. **Заголовки секций — с прописной, после заголовка пустая строка**, во всех + индексах. Написание канонических секций и отбивку правит `check --fix`; он + же сводит написание секции в мете файла с заголовком индекса. +6. **Язык проектных текстов** — [language.md](language.md), общий дом для + документов канона, задач, решений ADR и записок разведки: информационный + стиль (глагол вместо отглагольного существительного, активный залог, факт + вместо оценки, стоп-слова, параллельность), таблицы англицизмов и жаргона и + то, что из стиля отброшено намеренно. Проектных файлов не добавляет и + раскладку не меняет — это правила письма, а не новый слот. +7. **Умолчание профиля ревью сменилось** — это не раскладка, но проектный текст + под него уже написан. `standard` стал рабочим умолчанием: миграция схемы, + публичный контракт и инвариант ступень больше **не** поднимают, `wide` + означает новое понятие или структурную единицу. Подраздел «Триггеры профиля» + в `docs/review.md` остаётся на месте, но его содержимое надо перечитать. + +**Что переехало:** `docs/tasks/PLAN.md` → `docs/tasks/ROADMAP.md`; достигнутая +цель — из небытия в секцию `Готово`: `close <цель> --implemented` удаляет файл, но +**оставляет строку с датой**. Прежде роадмап отвечал только «что осталось», и +половину его вопроса вели прозой руками. Вместе с +файлом переименован ключ конфига `tasks.plan` → `tasks.roadmap` и токены +команд: `--index plan` → `--index roadmap`, `init --plan-sections` → +`--roadmap-sections`, `init --plan` → `--roadmap`. Старый ключ в +`docs/.pm.json` не игнорируется молча — `tasks.py` останавливается и называет +переименование. + +**Что удалено:** тип `[epic]`. Он был зонтиком между целью и задачами; зонтиком +стала цель, а слишком крупный шаг дробится на шаги помельче под ней. Ноль +употреблений на 97 записей двух живых проектов. + +**Что сделать проекту:** + +1. `git mv docs/tasks/PLAN.md docs/tasks/ROADMAP.md`. +2. Починить ссылки на прежнее имя: `grep -rn 'PLAN\.md' docs/ CLAUDE.md` — + заголовок самого файла («# План» → «# Роадмап»), строка в `docs/tasks/BACKLOG.md`, + упоминания в `docs/passport.md` и в телах задач. +3. `docs/.pm.json`: ключ `tasks.plan`, если он там был, — в `tasks.roadmap`. +4. Проставить род работы живым задачам: `python3 tasks.py check --dir docs/tasks` + перечислит те, у кого его нет. Задним числом весь беклог не переоформляется — + род нужен к взятию, так что порядок такой: сперва то, что берётся в ближайший + спринт, остальное по ходу переоценки. +5. Дописать раздел «Затрагивает» — тем же порядком и по той же причине: сперва + набор спринта, остальное по мере того, как задача попадает в работу. +6. Перечитать «Триггеры профиля» в `docs/review.md`: строки вида «миграция → + `deep`» теперь дублируют умолчание с обратным знаком. Оставить там только то, + что для этого проекта считается **новым понятием** и **правилом + идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю + частоту полного набора уточнением. +7. Переименовать секции роадмапа: `порядок` → `Запланировано`, `темы` → + `Направления`; завести `Готово` **первой** и `Разработка` последней + (порядок секций поменялся в версии 4 — если едешь сразу на неё, заводи + `Готово` последней и не переставляй дважды). + Прозаические разделы вроде «Что уже пройдено», которые велись руками, + разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно), + обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе + проверка считает секцией, и теперь `check` называет чужую секцию ошибкой. +8. Переформулировать цели ответом на **«что приложение будет уметь»**: не + «Работа со слиянием», а «Исход слияния не зависит от порядка доставки». + Свойство поведения — законная цель. Цель, которая не про приложение + (процесс, инструмент), переезжает в `Разработка`. +9. `[epic]`, если он в проекте заводился: это либо цель, либо набор задач под + общей целью. `check` назовёт его неизвестным типом. +10. Прогнать `python3 tasks.py check --dir docs/tasks --fix`: он поднимет + написание канонических секций, поставит отбивку после заголовков и сведёт + секцию в мете файлов с заголовками индексов. Секции беклога проект + переименовывает сам — их имена он выбирал, и трогать их скрипт не вправе. +11. Переписать заголовки задач в форму действия — по мере того, как задача + попадает в работу, а не «заодно»: `check` печатает их число, а `task-form` + предложит формулировки на замену пачкой. +12. Прочитать [language.md](language.md) — и **ничего не переписывать задним + числом**. Правила языка применяются к тому, что пишется и правится сейчас; + сплошная вычитка старых документов стоит дороже, чем даёт. +13. `docs/.pm.json`: `"canon": 3`. + +## Версия 2 — 2026-08-03 + +Шапка записи ADR — мета-блоком общей формы, и у статуса появился объявленный +дом. Раскладка не менялась: правка касается одного шаблона. + +**Что добавилось:** поле `- **Статус:**` в шапке `docs/adr/template.md` — +`заменено на ADR-…` либо `устарело`, у активной записи поля нет. Правило +«старая запись получает статус» было и раньше ([canon.md](canon.md), `adr/`), +но места под него шаблон не отводил: каждая запись изобретала своё, а колонка +«Статус» таблицы `adr/README.md` брала его оттуда, где он у каждого свой. + +**Что переехало:** поля `Дата` и `Источник` в шаблоне стали жирными +(`- **Дата:**`, `- **Источник:**`) — та же форма, что у меты задачи и у записи +журнала дефектов: поле на строку, имя жирным. + +**Что удалено:** ничего. + +**Что сделать проекту:** + +1. Привести `docs/adr/template.md` к скелету версии 2 + ([skeletons.md](skeletons.md), раздел `docs/adr/template.md`). +2. В существующих записях `docs/adr/ADR-*.md`: жирным поля шапки; если статус + записан прозой или заголовком — перенести его полем `- **Статус:**` в шапку + и сверить с колонкой «Статус» таблицы в `docs/adr/README.md`. +3. `docs/.pm.json`: `"canon": 2`. + +## Версия 1 — 2026-08-03 + +Первая версия. Проект любой прежней раскладки приводится к ней скиллом `canon` +в режиме `adopt`, а не `upgrade`. + +**Что вводится:** раскладка целиком — см. [canon.md](canon.md). + +**Что сделать проекту, который приходит из свободной раскладки:** + +1. `docs/.pm.json` с `{"canon": 1}` и путём миграций, если БД есть. +2. Скелет канона целиком; незаполненное — одной честной строкой. +3. `docs/specs/` разобрать: поведение — в `openspec/specs/`, обзор — в + `docs/architecture.md`, знание о чужих системах — в `docs/research/`. + Дубли capability удалить, сверив поимённо. +4. `docs/plan.md` → `docs/tasks/PLAN.md`, шаги плана — целями в «порядок». +5. `BRIEF.md` → `docs/passport.md`. +6. `docs/backlog/` → `docs/tasks/`. +7. `docs/review-journal.md` или `docs/review/journal.md` → `docs/review.md`, + плюс раздел настройки конвейера. +8. `docs/drafts/` растворить: идея → задача `[idea]`, намеренный отказ → ADR, + порядок работ → `PLAN.md`. +9. `docs/review-brief.md`, если заводился, удалить: его разделы разошлись по + документам канона. +10. `conventions.md` → `conventions/`, `local-research.md` → `research/`. +11. Завести `docs/security.md` с периметром первой строкой и `docs/adr/`. +12. В `CLAUDE.md`: severity рядом с каждым инвариантом; семантика гейта (чем + краснеет безусловно, где логи, чего в нём нет и кто тогда гоняет дорогое); + **имя основной ветки**; запреты с путями; где `testdata` и куда писать + временное; **что считается необратимым**; общий станок; ориентир по размеру + спринта. Убрать раздел «Процесс», если он пересказывает пайплайн. +13. В `openspec/config.yaml` оставить только нужды генерации и ссылки. +14. Добавить шаг `docs.py check` в гейт проекта. + +**Копии правил в шаблонах, которые версия 1 уносит в проект** — их правка в +каноне обязана появляться здесь отдельной версией: + +| Что копируется | Дом определения | +| --- | --- | +| форма записи журнала дефектов в `docs/review.md` | `av-dev-code/skills/review/references/review-journal.md` | +| правило заведения ADR в `docs/adr/README.md` | [canon.md](canon.md), раздел `adr/` | diff --git a/av-dev/skills/task-track/references/changelog.md b/av-dev/skills/doc-canon/references/changelog-tasks-before-merge.md similarity index 74% rename from av-dev/skills/task-track/references/changelog.md rename to av-dev/skills/doc-canon/references/changelog-tasks-before-merge.md index aa19cd5..a01ccda 100644 --- a/av-dev/skills/task-track/references/changelog.md +++ b/av-dev/skills/doc-canon/references/changelog-tasks-before-merge.md @@ -1,21 +1,12 @@ -# Журнал версий формата задач +# Журнал версий формата задач до слияния плагинов -Одна запись на версию. Проект знает свою версию из ключа `tasks` в `<каталог -задач>/.tasks.json`; повышение (`upgrade` в [SKILL.md](../SKILL.md), раздел -«Версия формата») идёт по записям снизу вверх от версии проекта до текущей и -делает то, что в них названо. +**Журнал закрыт.** У каталога задач была своя версия, пока плагином его ведал +`av-dev-tasks` и ставился он отдельно. Версия теперь одна на всю раскладку — +действующий журнал [changelog.md](changelog.md), и переезд числа описан его +записью 1. -Правило записи: **что добавилось, что переехало, что удалено, что сделать -проекту**. Без последнего пункта запись бесполезна — по ней и работает -повышение. - -Версия — целое число. Обратной совместимости у формата нет: есть «приведён» и «не -приведён». - -**Это журнал формата задач, а не канона документов.** Числа у них разные и -двигаются порознь: плагин `av-dev-tasks` ставится в одиночку, и у проекта без -`av-dev-docs` версии канона нет вовсе. Журнал канона — -`references/changelog.md` скилла `av-dev-docs:canon`. +Запись ниже не переписана под нынешние имена: она описывает состояние, которое +было. --- diff --git a/av-dev/skills/doc-canon/references/changelog.md b/av-dev/skills/doc-canon/references/changelog.md index 069cdc9..6b684c1 100644 --- a/av-dev/skills/doc-canon/references/changelog.md +++ b/av-dev/skills/doc-canon/references/changelog.md @@ -1,790 +1,82 @@ -# Журнал версий канона +# Журнал версий раскладки -Одна запись на версию. Проект знает свою версию из `docs/.docs.json`; `canon -upgrade` идёт по записям снизу вверх от версии проекта до текущей и делает то, -что в них названо. Записи ниже версии 13 зовут этот файл прежним именем, -`docs/.pm.json`, — так и было на день записи, и переписывать историю мы не -станем; переименование делает запись 13. - -**Каталог задач этим журналом не повышается.** У него своя версия формата и свой -журнал — `references/changelog.md` скилла `av-dev-tasks:tasks`. Записи 8, 11 и 12 -трогали его в те времена, когда своего числа у него не было; впредь запись канона -вправе позвать соседа, но не двигать его версию. +Одна запись на версию. Проект знает свою версию из ключа `version` в +`.av-dev.toml`; `canon upgrade` идёт по записям снизу вверх от версии проекта до +текущей и делает то, что в них названо. Правило записи: **что добавилось, что переехало, что удалено, что сделать проекту**. Без последнего пункта запись бесполезна — по ней и работает `upgrade`. -Версия — целое число. Обратной совместимости у канона нет: есть «приведён» и «не -приведён». +Версия — целое число. Обратной совместимости нет: есть «приведён» и «не +приведён». Версия **одна на всю раскладку** — и на документы канона, и на +каталог задач: ведёт их один плагин, и второе число означало бы только вопрос, +по какому журналу повышать. + +**До слияния журналов было два**, и нумерация в них своя: +[changelog-before-merge.md](changelog-before-merge.md) — канон документов, +версии 1–14; [changelog-tasks-before-merge.md](changelog-tasks-before-merge.md) — +формат задач, версия 1. Оба **закрыты и не переписаны**: адрес, верный на день +записи, там и остался. Проект, отставший от канона 14, идёт сперва по ним, а +потом по этому журналу — порядок назван в записи 1. --- -## Версия 14 — 2026-08-11 +## Версия 1 — 2026-08-13 -У ADR стало два законных источника. Прежде запись цитировала только архивный -`design.md`, то есть решение, принятое по ходу изменения. Решение, принятое -**разведкой** — намеренный отказ, выбор подхода, «проверили и не делаем», — не -имеет `design.md` по построению: change по нему не заводится никогда. Триггер -канона такое решение ловит («намеренный отказ от очевидного подхода»), а дома у -него не было, и оно оседало в записке разведки или в переписке. +Три плагина — `av-dev-docs`, `av-dev-tasks` и `av-dev-code` — слились в один, +`av-dev`. Раскол делался под раздельную установку: проект мог взять учёт работ +без документов канона или конвейер без обоих. Практикой посылка не подтвердилась +— подмножество не понадобилось ни разу, — а платился раскол помеченными копиями +общих правил и ветками деградации на каждый вызов соседа. -**Что изменилось.** `adr/` принимает второй источник — записку разведки. Правило -«промоут, а не второе сочинение» не тронуто: запись по-прежнему цитирует уже -написанное и **называет источник**, изменилось только то, что источников два. -Следом сказали то же самое: карта домов, разрез проверки `doc-consistency`, вход -и устав самого агента, скелеты `docs/adr/README.md` и `docs/adr/template.md`. +**Что переехало в проекте.** Служебных файла было два, стал один: -**Почему это версия, а не правка текста.** Два следствия уезжают в репозиторий -проекта. По карте домов судит агент согласованности — прежняя редакция читала ADR -со ссылкой на записку разведки как нарушение; а скелеты `adr/` лежат в проекте -файлами и говорят там от имени канона. - -**Что сделать проекту.** - -1. Ничего с существующими записями: прежние ADR ссылаются на `design.md`, и это - по-прежнему верно. -2. **Поднять шапку `docs/adr/README.md`**: «промоут поверх архивного `design.md`» - → «промоут поверх уже написанного», с обоими источниками. Точный текст — в - [skeletons.md](skeletons.md), раздел `docs/adr/README.md`. -3. **Поднять `docs/adr/template.md`**: строка `- **Источник:**` называет два - возможных источника. -4. `docs/.docs.json`: `"canon": 14`. - -**Чего делать не надо.** Заводить ADR задним числом по старым разведкам. Запись -заводится, когда решение принимается, а не когда о нём вспомнили: сочинённое -через полгода обоснование — ровно то «второе сочинение», против которого правило -и написано. - ---- - -## Версия 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 - -Спринты отменены. Работа идёт задача за задачей, и замороженный набор перестал -что-либо удерживать: он отвечал на вопрос «что делать дальше», а между наборами -на этот вопрос не отвечал никто. - -**Что изменилось.** Индексов задач два вместо трёх: `SPRINT.md` упразднён. -Приоритет стал тем, чем он и является, — **порядком строк в `BACKLOG.md`**: -первая строка секции это то, что делают следующим. Назначает порядок человек, -машина его не выводит; двигают его `move --after` и `move --first` с причиной. - -Гейт готовности записи стоял на взятии задачи в спринт — единственном месте, где -её судили целиком. Момент нужен и без спринта: теперь это команда -`tasks.py ready <слаг>`, и зовёт её тот, кто берёт задачу в работу. - -Ритуал между спринтами (`av-dev-tasks:session`) стал скиллом груминга -(`av-dev-tasks:groom`): два вопроса — что сейчас самое важное и что перестало -быть важным. - -**Что сделать проекту.** - -1. **Вернуть задачи из набора в беклог и снести `SPRINT.md`.** Порядок такой: - `git rm tasks/SPRINT.md`, затем `tasks.py check --dir tasks --fix`. Строки - набора после удаления файла становятся бездомными, и `--fix` возвращает их в - беклог **в конец своей секции** — с пометкой, что позицию назначает человек. - Наоборот делать нельзя: `check` без удалённого файла увидит третий индекс и - станет ругаться на него, а не чинить. -2. **Снять теги `sprint:<слаг>`** с записей — `tasks.py edit <слаг> --rm-tag - sprint:<слаг>`. Тег больше никем не читается, а `check` о нём молчит: он - законный свободный тег. Пропущенный вреда не сделает, но и пользы не несёт. -3. **Расставить порядок** — первый груминг: `av-dev-tasks:groom`. После шага 1 - очередь состоит из того, что машина поставила в конец, то есть очереди нет - вовсе. Пока порядок не назначен, «что делать дальше» по-прежнему без ответа. -4. Поправить упоминания спринта в `CLAUDE.md` проекта, если они были: слот - «общий станок» переехал в груминг под именем «что считается сломанным», - ориентир «5–8 задач в спринте» стал ориентиром размера порции разбора. -5. `docs/.pm.json`: `"canon": 12`. - -**Чего делать не надо.** `REJECTED.md`, `ROADMAP.md` и файлы `items/` не -меняются: спринт жил только в собственном индексе и в тегах. - ---- - -## Версия 11 — 2026-08-09 - -Каталог задач уехал из `docs/` в корень репозитория. Версия 8 отпустила его из -канона — перестала требовать, перестала проверять, — но место он занимал всё то -же, `docs/tasks/`. Полдела: каталог, принадлежащий одному плагину, лежал внутри -дерева, которым владеет другой. Проекту, поставившему учёт работ без канона -документов, приходилось заводить `docs/` ради одной вложенной папки. - -**Что изменилось.** Дом задач — `tasks/` в корне репозитория. `tasks.py` ищет его -там первым; `docs/tasks/` и `doc/tasks/` остаются в списке поиска для -непереехавших проектов, а `init` заводит только в корне. Настройки — там же, -`tasks/.tasks.json`. - -**Что осталось терпимым.** `docs.py` по-прежнему не считает `docs/tasks/` файлом -вне канона: непереехавший проект не должен получать выдуманную ошибку вдобавок к -этой записи, которая и так велит ему переехать. - -**Что сделать проекту.** - -1. `git mv docs/tasks tasks` — одним коммитом вместе с шагом 2, чтобы ссылки не - жили битыми между коммитами. -2. **Починить относительные ссылки внутри записей.** Файл `tasks/items/x.md` - стал на уровень ближе к корню: `../../passport.md` в теле записи теперь - `../docs/passport.md`. Тот же сдвиг у ссылок из индексов. Это самая тихая - часть переезда: битая относительная ссылка не мешает `tasks.py check`, её - ловит только `docs.py check` и только у документов канона. -3. Проверить ссылки **на** задачи снаружи: `CLAUDE.md`, `README.md`, гейт, - `docs/review.md`. Путь `docs/tasks/...` в них теперь ведёт в никуда. -4. Поправить путь в гейте: `tasks.py check --dir tasks`. -5. `docs/.pm.json`: `"canon": 11`. - -## Версия 10 — 2026-08-09 - -Проверка формы `config.yaml` ушла к тому, кто файл заводит. Версия 9 перенесла в -конвейер настройку OpenSpec и честно назвала остаток: форма и сторож версии -остались в `docs.py`, то есть у файла было два плагина — один заводит, другой -проверяет. Остаток закрыт. - -**Что появилось.** Скрипт `openspec.py` в скилле `av-dev-code:openspec`, две -команды: `check --dir <корень>` — форма в проекте, `form` — сверка слепка с живым -OpenSpec. Коды выхода те же, что у `docs.py` и `tasks.py`. - -**Что удалено из `docs.py`.** Константы `OPENSPEC_*`, проверка формы, сторож -версии и подкоманда `openspec-form` — 252 строки. Скрипт канона про -`openspec/config.yaml` не говорит теперь ничего; `openspec/specs/` он по-прежнему -знает, потому что это дом темы `requirements` и часть карты тем. - -**Что стало лучше по дороге.** Адреса `docs/passport.md` и `CLAUDE.md` требуются -теперь **только к тем документам, которые в проекте есть**. Прежняя проверка -требовала их безусловно, то есть на проекте без канона документов требовала -битую ссылку. Теперь отсутствие документа — строка «не проверялось» с указанием, -что без канона конвейер работает вслепую. - -**Что осталось за каноном.** Один вопрос, и это не форма: не пересказан ли в -`context` документ, у которого есть свой дом. Разрез — утверждение, опровергаемое -открытием другого файла, против строки «открой такой-то файл»; машине он не -виден, судит агент `doc-consistency`, и `config.yaml` у него во входе. - -**Что сделать проекту.** - -1. Заменить в гейте и в скриптах `docs.py openspec-form` на `openspec.py form`. - Подкоманды больше нет: прежний вызов упадёт ошибкой употребления (код 2), а не - промолчит. -2. **Добавить в гейт шаг `openspec.py check`, если проект работает по OpenSpec.** - Форму раньше проверял `docs.py check` заодно; теперь он о ней молчит, и без - отдельного шага незаменённый пример в `config.yaml` перестанет ловиться. Это - главная потеря этого повышения, и она тихая. -3. Проект по OpenSpec без установленного `av-dev-code` — форму не проверяет - никто. Либо поставить плагин, либо назвать это принятым риском вслух. -4. `docs/.pm.json`: `"canon": 10`. - -## Версия 9 — 2026-08-09 - -OpenSpec уехал в конвейер. Каталог `openspec/` версией 7 был объявлен слотом -канона: `init` его заводил, `adopt` тоже, образец `config.yaml` лежал в скелетах, -а отсутствие каталога `docs.py` считал отказом. Разрез был проведён не там. По -OpenSpec работает конвейер — без каталога не запускаются ни `opsx:propose`, ни -ревью дизайна, ни сверка требований, — а канон документов о нём только -высказывался. Проект, которому конвейер не нужен, получал отказ за отсутствие -того, чем не пользуется. - -**Что появилось.** Скилл `av-dev-code:openspec`: заводит каталог, заменяет -закомментированный пример в `config.yaml` настройкой, объясняет разрез между -ссылкой и пересказом. Образец файла переехал туда же — в -`references/config-skeleton.md` того скилла. - -**Что изменилось.** `init` и `canon adopt` OpenSpec больше не заводят, а **зовут -скилл конвейера**; вызов не разрешился — плагина конвейера нет, и это строка -доклада, а не поломка. Отсутствие `openspec/` для `docs.py check` стало -неприменимостью вместо отказа: остальные четыре проверки формы идут только при -живом каталоге. - -**Что осталось на месте и почему.** Проверка формы `config.yaml` и сторож версии -(`docs.py openspec-form`) пока живут в скрипте канона — переносить их значит -заводить в конвейере свой скрипт, а этого у него нет ни одного. Разрез названного -это не отменяет, но и не завершает: **у файла сейчас два плагина — один заводит, -другой проверяет**, и это временное состояние, а не задуманное. - -**Что сделать проекту.** - -1. Ничего не переносить: файлы проекта эта версия не двигает. Меняется только то, - кто их заводит. -2. Проверить, что плагин `av-dev-code` установлен, если проект работает по - OpenSpec. Без него `docs.py check` про каталог промолчит — и молчание это - законное, так что отсутствие настройки перестанет ловиться само. -3. Проект **не** работает по OpenSpec: убедиться, что `openspec/` нет, и - перестать держать его пустым ради проверки. Она больше не требует каталога. -4. `docs/.pm.json`: `"canon": 9`. - -## Версия 8 — 2026-08-09 - -Канон отпустил каталог задач. Плагин `av-dev-pm` расколот на `av-dev-docs` -(документы) и `av-dev-tasks` (учёт работ), и каждый теперь ставится сам по себе. -Пока владелец был один, `docs/tasks/` числился слотом канона: `docs.py` требовал -каталог, звал внутрь чужой скрипт и выдавал его дрейф за свой, а настройки задач -жили ключом `tasks` в `docs/.pm.json`. Для проекта, поставившего только документы, -всё это — отказ на ровном месте: задач он не ведёт, и требовать их не за что. - -**Что изменилось.** Каталог задач канону не принадлежит; канон резервирует ему -место в `docs/` и внутрь не смотрит. `docs.py` больше не проверяет согласованность -задач вовсе — это делает `tasks.py` сам, командой своего плагина. Дом настроек -каталога задач — `<каталог задач>/.tasks.json`; ключ `tasks` в `docs/.pm.json` -читается, только пока своего файла нет, и об этом говорится замечанием. - -**Что удалено.** Проверка `check_tasks` из `docs.py` и ключ `"tasks"` из скелета -`docs/.pm.json`. - -**Что сделать проекту.** - -1. Перенести настройки задач: содержимое ключа `"tasks"` из `docs/.pm.json` — в - `docs/tasks/.tasks.json` тем же объектом. Ключа в проекте нет (имена файлов - и заголовков умолчательные) — переносить нечего, шаг пропускается. -2. Удалить ключ `"tasks"` из `docs/.pm.json` после переноса. Оставленный он не - читается, и `tasks.py` скажет об этом замечанием на каждом прогоне. -3. Проверить, что согласованность задач по-прежнему кто-то гоняет: раньше её - тянул за собой `docs.py check`, теперь — только `tasks.py check`. **Если в - гейте проекта стоял один `docs.py`, добавить туда второй шаг** — иначе дрейф - индексов перестанет ловиться молча, и это самая вероятная потеря на этом - повышении. -4. Установить оба плагина, если нужны оба: `av-dev-docs` и `av-dev-tasks` - вместо прежнего `av-dev-pm`. Прежний из `enabledPlugins` убрать. -5. `docs/.pm.json`: `"canon": 8`. - -## Версия 7 — 2026-08-07 - -`openspec/` был предпосылкой, о которой канон говорил, но за которой не следил. -Каталог назван в раскладке, `openspec/specs/` объявлен домом темы `requirements`, -`config.yaml` описан абзацем — а заводил всё это человек руками, и проверялось -из перечисленного ничего. Заведение нового проекта проходило мимо: `init` -собирал документы канона и оставлял проект без каталога, без которого не работают -ни `opsx:propose`, ни ревью дизайна, ни сверка требований. - -Хуже отсутствия оказался файл из коробки. `openspec init` кладёт `config.yaml`, -где `context` и `rules` — закомментированный пример на английском. Такой файл -читается как настроенный: он есть, он валиден, имя правильное. Работает он как -пустой, и узнаётся это по предложению, написанному на другом языке, с -capability по имени пакета и без единого `SHALL`. - -**Что изменилось:** - -1. **`init` заводит OpenSpec сам** — `openspec init --tools claude`, до первого - документа канона. Команда названа в каноне поимённо, потому что её печатает - отказ `docs.py`. -2. **У `openspec/config.yaml` появилась каноническая форма** и скелет в - `skeletons.md`. Содержание — только то, что нужно **в момент порождения - артефакта**: язык, правила именования capability, придирки валидатора и - **адреса** документов канона. Пересказ паспорта, инвариантов, конвенций и - правил ревью в него не переносится. -3. **`docs.py check` проверяет пять вещей:** каталог `openspec/` есть; файл - называется `config.yaml` (`config.yml` OpenSpec читать не станет и об этом не - сообщит); `context` и `rules.specs` не остались примером, а правила для - `specs` называют `SHALL`; `context` называет `passport` и `CLAUDE.md`; ключи - под `rules:` — имена артефактов схемы, а не опечатки. -4. **За свежестью формы следит машина, а не память.** Схема и перечень - артефактов — слепок чужого инструмента; `check` сравнивает `major.minor` - установленного OpenSpec с версией, на которой форма сверялась, и при - расхождении даёт замечание. Перепроверяет `docs.py openspec-form`, и чинится - расхождение **в плагине, а не в проекте**. -5. **Шестое проверяет агент.** Отличить ссылку на документ от пересказа документа - машина не умеет — это работа `doc-consistency`, и в таблице «Что проверяет - машина, а что человек» она стоит строкой. - -**Что переехало:** ничего в раскладке `docs/`. Ни один файл не переименовывается -и не перемещается. - -**Что сделать проекту:** - -1. Нет `openspec/` — завести: `openspec init --tools claude`. Команда кладёт ещё - и `.claude/skills/openspec-*` с `.claude/commands/opsx/*`; это её нормальная - работа, удалять их не надо. -2. Открыть `openspec/config.yaml` и привести к скелету из - [skeletons.md](skeletons.md): блок `context` с языком, правилами именования - capability, требованием `SHALL` и **адресами** `docs/passport.md` и - `CLAUDE.md`; блок `rules` с четырьмя правилами для `specs`. -3. **Вычистить из `context` пересказ.** Инварианты, перечень конвенций, состав - шагов гейта, правило выбора метки и состав проходов ревью — заменить ссылкой - на дом. Признак пересказа простой: строку можно опровергнуть, открыв другой - файл проекта. -4. Проверить имя файла: `config.yml` переименовать в `config.yaml`. Если жили оба - — содержимое `.yml` до сих пор не читалось никем, и переносить из него нужно - именно то, чего нет в `.yaml`. -5. `docs/.pm.json`: `"canon": 7`. - ---- - -## Версия 6 — 2026-08-07 - -Версия 5 объявила: **каждый документ `docs/` — тема ревью**. Правило оказалось -верным ровно наполовину и потому вредным целиком. Паспорт и схему хранилища -ревью читает, но темами они не являются — они задают границу, по которой судит -чужая тема. Журнал решений и журнал наблюдений ревью изменения не нужны вовсе: -ADR объясняет прошлое решение, а не предъявляет требование к изменению. - -Разметчик, применявший плоское правило буквально, обязан был либо завести -фантомные темы `passport`, `adr`, `database`, `research` и продублировать ими -работу тем `architecture` и `operations`, либо потерять четыре документа молча — -а молчащая потеря и есть то, против чего канон написан. - -**Что изменилось:** - -1. **Три категории документов вместо одной.** Разрез проверяемый: можно ли по - документу сказать «в этом изменении сделано не так»? **Тема** — да, прямо - (`conventions`, `security`, `architecture`, свои документы проекта). - **Источник темы** — нет, но он задаёт границу для чужой темы (`passport.*` → - `architecture`, `database.*` → `operations`, `CLAUDE.md` → `autotests`, - `openspec/specs/` → `requirements`). **Процессный документ** — нет, он про то, - как мы работаем (`tasks/`, `review.*`, `adr.*`, `research.*`, `.pm.json`). -2. **Категории `источник` и `процессный` закрыты, категория `тема` открыта.** - Прежде открытым был весь список, и «не темы ровно две» противоречило - собственной раскладке канона. Теперь пополняется только одно множество, и - документ, которого нет в раскладке, — однозначно своя тема проекта. -3. **`adr/` и `research/` уходят из входа ревью изменения.** Прогон их больше не - открывает. Проверяться они не перестали: ADR без ссылки на архивный - `design.md`, замена без парного статуса, число без провенанса — это по-прежнему - работа `doc-consistency` и `doc-code-drift`, на сессии между спринтами. -4. **`docs.py` печатает категорию в отказе.** «Нет источника passport» читается - иначе, чем «нет темы security». Обязательность при этом не изменилась: - заводятся все документы одинаково и с первого дня. -5. **У задачи появилась метка — `small`, `medium` или `large`.** Это итог - классификации и **единственный вход, по которому конвейер выбирает - исполнителей** на обеих стадиях ревью. Прежние имена `quick`, `standard` и - `wide` описывали глубину прогона, то есть свойство ревью; метка описывает - **задачу** — а выбирают по ней одно и то же. Слово «ступень» уходит: - у одной вещи одно имя. -6. **Метка выводится из двух осей и не равна ни одной из них.** Размер (малое, - среднее, крупное) и сложность (знакомое, незнакомое); метка — максимум по - ним. Малое **незнакомое** изменение получает `large`, трогая один узел, — - поэтому размер и метка пишутся отдельными строками, и выводить одно из - другого нельзя. - -**Цена, записанная явно:** расхождение изменения с записанным решением прогоном -больше не ловится. Раньше архитектурный проход мог сказать «здесь отменено -решение ADR-2026-03-11, парного статуса нет»; теперь это скажет только сверка -документации. Сделка сознательная: чтение всего каталога решений оплачивалось на -каждой задаче, а срабатывало на единицах. - -**Что переехало:** ничего в раскладке. Ни один файл не переименовывается и не -перемещается. - -**Что сделать проекту:** - -1. `docs/review.*`, подраздел «Вопросы по темам»: убрать вопросы, адресованные - `passport`, `database`, `adr`, `research` и `review` — **ни одно из этих имён - больше не тема**. Под каноном 5 темой был каждый документ `docs/`, поэтому - такие вопросы там законны и почти наверняка есть. Переадресовать: - про границу домена и про решение → `architecture`; про хранилище, настройку и - измеренное число → `operations`. Вопрос, который никуда не переадресовывается, - удалить, а не оставить висеть: адресованный несуществующей теме, он не - задаётся никем и молча. -2. Там же, «Недоступно проверке»: те же пять имён убрать из разнесения по темам, - переразнеся содержимое по оставшимся. -3. Там же: подраздел **«Триггеры профиля» → «Триггеры метки»**, и разнести его - на **три** списка вместо двух — «крупное здесь» (про объём), «незнакомое - здесь» (про форму решения) и «мелкое здесь» (опускает до `small`). Раньше - первые две оси были склеены в один список, и потому объём в правило по факту - не входил. -4. **Переименовать метки прогона везде, где проект их называет** — в «Триггерах - метки», в «Недоступно проверке», в журнале дефектов: `quick` → **`small`**, - `standard` → **`medium`**, `wide` → **`large`**. Метка это итог классификации - задачи, и три её значения — часть общего словаря канона и конвейера. Слово - «ступень» из документов уходит: у одной вещи одно имя. -5. Проверить, что свои темы проекта не совпадают именем с закрытыми категориями: - `docs/passport/`, `docs/adr/`, `docs/research/`, `docs/database/`, - `docs/review/` — это слоты канона, а не свои темы, и своим смыслом их - наполнять нельзя. -6. Ничего не заводить и не удалять: раскладка канона 6 совпадает с раскладкой - канона 5 файл в файл. -7. `docs/.pm.json`: `"canon": 6`. - ---- - -## Версия 5 — 2026-08-06 - -Канон перестал быть списком файлов и стал **списком тем ревью**. Раскладка та же, -но читается иначе: документ в `docs/` — это направление проверки, а не просто -текст. Отсюда три правки, и все три развязывают то, что раньше было жёстко -сцеплено. - -**Что изменилось:** - -1. **Тема живёт файлом или каталогом, на выбор проекта.** `docs/security.md` и - `docs/security/` — одно и то же; тема разрослась, стала каталогом с - `README.md` — канон не сменился и версия не двинулась. Прежде форма была - задана поимённо: `conventions`, `research` и `adr` обязаны были быть - каталогами, остальные — файлами, и обосновать это было нечем. Обе формы сразу - — ошибка: два дома для одного факта расходятся молча. -2. **Список тем открытый.** Всё, что проект кладёт в `docs/`, становится темой - ревью и попадает в план каждого прогона; именной оптики у такой темы нет, её - разбирает общий проход конвейера, заведённый ровно за этим. - Прежде `docs.py` называл незнакомый файл «вне канона» — теперь называет своей - темой проекта и перечисляет их в отчёте. Не темы ровно две: `docs/tasks/` и - `docs/review.*`. -3. **`AGENTS.md` рядом с `CLAUDE.md` — законно.** Он почти стандарт; обязателен - по-прежнему только `CLAUDE.md`, но если лежат оба, читаются оба, и проверки - канона смотрят на второй так же, как на первый. - -**Что переехало:** - -- в `docs/review.*`: **«Вопросы к проходам» → «Вопросы по темам»**, форма - `<тема>: <вопрос> (<провенанс>)`. Причина не косметическая: вопрос, - адресованный проходу, перестал задаваться молча в тот день, когда тот уехал в - верхнюю ступень ревью. Тема переезд прохода переживает, имя прохода — нет; -- там же **«Недоступно проверке» — по темам**, оба подраздела. - -**Что сделать проекту:** - -1. Ничего не переименовывать, если всё уже разложено по канону 4: обе формы - дома законны, и текущая — одна из них. -2. `docs/review.*`, подраздел «Вопросы к проходам»: переименовать в «Вопросы по - темам» и переадресовать каждый вопрос теме вместо имени прохода. Темы ядра — - `requirements`, `autotests`, `conventions`, `architecture`, `security`, - `operations`. -3. Там же «Недоступно проверке»: разнести обе половины по темам. -4. Проверить, не лежит ли в `docs/` документ, который раньше считался лишним и - потому не заводился. Теперь он законен и станет темой ревью — это и есть - способ добавить проверку, которой в конвейере нет. -5. `docs/.pm.json`: `"canon": 5`. -6. Позвать судей `doc-consistency` и `doc-code-drift` — шагом 6 `upgrade`. - -## Версия 4 — 2026-08-05 - -Две правки, обе про то, как читается каталог задач. Первая — секция роадмапа -переименована, и вместе с именем расширен её смысл; достигнутое переехало вниз. -Вторая — **у каждой записи появился тип, и тип определяет, что с записью можно -делать**. Раскладка не меняется, файлов канона не прибавляется. - -**Что переехало:** - -- секция роадмапа `Разработка` → **`Сопровождение`** (англ. `Tooling` → - **`Operations`**). Прежнее имя называло слишком много: роадмап **весь** про - разработку, и секция с таким именем не отличалась от остальных ничем; -- **тип записи** — из префикса заголовка (`[goal]`/`[idea]`) и тега - `kind:<род>` в **поле меты `Тип`** первой строкой. Эмодзи в заголовке от него - производна; -- **поле места** у задачи: `Секция` → **`Категория`**. У цели остаётся `Секция`: - у задачи поле называет полку домена, в которую она вернётся из спринта, у цели - — часть роадмапа, то есть состояние очереди. Одно имя на два смысла их и - смешивало. - -**Что добавилось:** - -1. **Смысл секции расширен.** Было «инструмент и процесс», стало «чем держат - проект: инструмент, процесс, эксплуатация». Метрики, логи, инфраструктура, - выкладка и дежурство — сюда же. Расширение не косметическое: английское - `Operations` при узком смысле обещало бы эксплуатацию, а внутри лежал бы - линтер. -2. **Общий словарь трёх мест** — [canon.md](canon.md), раздел «Сопровождение и - эксплуатация». Сопровождение — всё, чем держат проект; эксплуатация — его - часть, работа системы на проде. `ROADMAP.md`, секция `Сопровождение` — план - работ; `architecture.md`, раздел «Эксплуатация» — как устроено сейчас; - эксплуатационный проход ревью — оптика проверки. Слить их в одно слово - нельзя: они отвечают на разные вопросы. Слово **«поддержка» не употребляется - вовсе** — в нём слышится помощь пользователю. -3. **Граница с возможностями проходит по тому, кто наблюдает.** «Приложение - сообщает о своём состоянии» — возможность приложения, её место среди прочих - целей. «Дежурный видит состояние на одном экране» — сопровождение. Одни и те - же метрики попадают в разные секции роадмапа, и это верно. -4. **Порядок секций стал каноническим**, и `Готово` переехало **вниз**: - `Запланировано` | `Направления` | `Сопровождение` | `Готово`. Достигнутое - копится — через год этой секции больше, чем всех остальных вместе, — и стоя - первой она отодвигает за экран то, ради чего роадмап открывают чаще всего. - Порядок проверяет `tasks.py check`, переставляет `check --fix`. -5. **Заголовок секции отбивается пустой строкой с обеих сторон.** Прежде - проверялась только строка после заголовка; перестановка секций двигает целые - блоки, и два заголовка оказываются вплотную. Правит `check --fix`. -6. **Тип — единственная ось записи, закрытый словарь из пяти значений:** - `goal` | `feature` | `fix` | `chore` | `research`. Осей было две — тип записи - (`goal`/`idea`/`task`) и род работы (`kind:` тегом), — но из двенадцати - клеток произведения законны были шесть, а алгоритм работы крепится к роду, а - не к типу. Оси схлопнуты. -7. **Тип задаёт схему тела:** какие разделы обязательны, какие допустимы, нужна - ли цель, берётся ли запись в спринт. Проверяет `sprint take`, замечания даёт - `check`. Два раздела новые: **`Воспроизведение`** у `fix` (не - воспроизводится — это `research`, а не `fix`; правило было записано и не - проверялось) и **`Вопрос` + `Куда ляжет ответ`** у `research` вместо - критериев приёмки (приёмка разведки — записанный ответ, и критерии в форме - «оракул: тест» ей натянуты). -8. **Тип `idea` упразднён.** Он значил не род работы, а состояние - незаполненности, а состояние типом быть не может. Теперь оно называется - честно: `research` без раздела «Вопрос» — **сырьё**. В спринт не берётся, как - и прежняя идея, лежит **в конце своей категории** (проверяет `check`, - переставляет `--fix`) и отбирается `list --raw`. Порядка «по важности» в - беклоге по-прежнему нет: этот порядок производен от типа, а не назначен - человеком. -9. **Алгоритм работы над каждым типом** — отдельным файлом, - `skills/tasks/references/task-<тип>.md`: схема, что проверяет машина, что - человек, и порядок шагов. -10. **Имена файлов проверяются.** Правило «текст русский, имена английские» - стояло в каноне и не было подкреплено ничем: `docs.py` имён не смотрел вовсе. - Теперь смотрит — кириллица и не-kebab-case **жёстко**, форма имени - `ADR-ГГГГ-ММ-ДД-slug.md` жёстко, транслит **эвристикой**, то есть - замечанием. Заодно из раскладки канона убраны плейсхолдеры `<тема>.md`, - приглашавшие называть файлы по-русски. -11. **Два агента вместо обещания.** В каноне была таблица «Что проверяет машина, - а что человек», и её правая колонка три версии описывала судью, которого не - существовало. Судьи заведены и разведены по глубине: **`doc-consistency`** - (документ ↔ документ ↔ openspec: факт в двух домах, прямое противоречие, - поведение в `architecture.md` вместо спек, ADR без ссылки и парного статуса, - число без провенанса, заглушка вместо честной строки); **`doc-code-drift`** - (документ ↔ код по закрытому перечню фактов). Оба зовутся раз в спринт на - сессии, а также после adopt и после upgrade, на весь канон разом. - -**Что сделать проекту:** - -1. Переименовать заголовок секции в `docs/tasks/ROADMAP.md`: `## Разработка` → - `## Сопровождение` (или `## Tooling` → `## Operations`, если индекс - английский). **`check --fix` этого не сделает**: регистр канонической секции - он правит сам, а чужую секцию только называет ошибкой — смысл за человеком. -2. Поправить поле `- **Секция:**` в файлах целей, которые в ней лежат. Порядок - именно такой: сперва заголовок, потом `python3 tasks.py check --dir - docs/tasks` покажет расхождение поимённо. -3. Перечитать состав секции: цели про выкладку, метрики, логи и инфраструктуру, - если они лежали в `Направлениях` за неимением места, переезжают сюда. -4. Прогнать `python3 tasks.py check --dir docs/tasks --fix`. За один проход он - переставит секции роадмапа в канонический порядок (`Готово` уедет вниз вместе - со всем содержимым), поправит отбивку заголовков и **переведёт записи на - типы**: перенесёт значение из тега `kind:` и префикса `[goal]`/`[idea]` в поле - `Тип`, снимет тег, поставит эмодзи в заголовок, переименует `Секция` → - `Категория` у задач и снесёт сырьё в конец категорий. -5. Разобрать то, что `--fix` вернул пометкой `НЕОДНОЗНАЧНО`. Главный случай — - **записи без типа**: заведённые до появления рода работы, они не несут ни - тега, ни префикса, и машина их не угадывает (`feature` от `chore` не - отличает). Проставить руками: `edit <слаг> --type …`. -6. Дописать новые обязательные разделы у задач, которые собираются в спринт: - `Воспроизведение` у каждого `fix`, `Вопрос` и `Куда ляжет ответ` у каждого - `research`. Не «заодно по всему беклогу», а порциями переоценки: `check` - ошибкой это не считает, отказывает только `sprint take`. Сколько задач готово - к взятию, печатает блок здоровья `check`. -7. Прогнать `python3 docs.py check`: он назовёт имена файлов не по правилу. - Кириллицу и не-kebab-case править обязательно, транслит — по решению - человека. **Переименование ADR это перенос ссылок**: слаг стоит в - `adr/README.md`, в `architecture.md` и в чужих документах, и делается одним - проходом, иначе останутся битые ссылки (их `docs.py` потом и покажет). -8. `docs/review.md`, подраздел «Триггеры профиля» — переписать целиком, он - отстал дважды. Снести перечень мест для `deep`: профиль упразднён вместе с - проходом независимой реализации, и перечень стал указателем в пустоту. - Оставшийся перечень перевести на новое правило: `wide` теперь означает не - «новое понятие», а **крупное или незнакомое** изменение и рассчитан на 5–10% - задач; отдельным списком назвать, что здесь считается **мелким** (это `quick`). - Форма подраздела — в [skeletons.md](skeletons.md). Там же проверить журнал - дефектов и «Недоступно проверке» на упоминания независимой реализации: класс - «форма решения, где спека выбора не сделала» переезжает в подраздел «перестали - проверять сознательно», а рядом с ним встаёт вторая честная строка — на - `quick` и `standard` не проверяется ничего, что требует запуска. -9. `docs/.pm.json`: `"canon": 4`. -10. Позвать **обоих судей** — `doc-consistency` и `doc-code-drift`, шагом 6 - `upgrade`. Пунктов выше десять, половина из них ручная, и именно здесь видно, - какие сделаны только наполовину: переименования секций и полей разводят - документы, а `check` сверяет число версии, а не существо. Первый прогон на - живом проекте вдобавок самый урожайный — правило единственного дома до сих пор - никто не проверял. Разбирать порциями, а не одним заходом. - -## Версия 3 — 2026-08-04 - -Роадмап стал **состоянием проекта**, а не очередью работ: цель — возможность -приложения, задача — шаг к ней, достигнутое из роадмапа не исчезает. Плюс род -работы, раздел «Затрагивает» и новое умолчание профиля ревью. Раскладка меняется -в одном файле, но переименование и смена секций тянут за собой ссылки, поэтому -шаги делаются одним заходом. - -**Что добавилось:** - -1. **Род работы** — тег `kind:<род>` в мете задачи, словарь закрыт: - `feature` | `fix` | `chore` | `research`. Обязателен у задачи, у цели - запрещён. `sprint take` без него отказывает, `check` о пропаже напоминает - замечанием. Определение — [canon.md](canon.md), раздел `tasks/`; смысл и - причина, почему тегом, — в SKILL.md скилла `tasks`, раздел «Род работы». -2. **Раздел «Затрагивает»** в теле задачи — перечень границ, которых изменение - касается (эндпоинт, таблица и миграция, формат на диске, публичный тип). Как - и критерии приёмки, требуется к взятию в спринт, а не к заведению. -3. **Секции роадмапа** — четыре вместо двух и **канонические**, в отличие от - секций беклога: `Готово` (достигнутые цели строкой с датой, без ссылки на - файл), `Запланировано` (очередь значима), `Направления` (очереди нет), - `Разработка` (инструмент и процесс, не возможности приложения). Английский - вариант — `Done` | `Planned` | `Directions` | `Tooling`, один язык на весь - индекс. Переименованию проектом не подлежат: у каждой свой смысл, и в первую - пишет сам `close`; `tasks.py check` проверяет состав. -4. **Форма заголовка записи** — по типу: задача отвечает на «что нужно сделать» - и пишется глаголом в неопределённой форме («Не отбрасывать молча лишние - символы»), цель — на «что приложение будет уметь», идея просто называет, о - чём она. `check` считает заголовки не в форме действия и печатает число в - блоке здоровья. Годность формулировки — не машине: её смотрит новый агент - `task-form` (форма записи, только чтение), а язык текста — `doc-wording`. -5. **Заголовки секций — с прописной, после заголовка пустая строка**, во всех - индексах. Написание канонических секций и отбивку правит `check --fix`; он - же сводит написание секции в мете файла с заголовком индекса. -6. **Язык проектных текстов** — [language.md](language.md), общий дом для - документов канона, задач, решений ADR и записок разведки: информационный - стиль (глагол вместо отглагольного существительного, активный залог, факт - вместо оценки, стоп-слова, параллельность), таблицы англицизмов и жаргона и - то, что из стиля отброшено намеренно. Проектных файлов не добавляет и - раскладку не меняет — это правила письма, а не новый слот. -7. **Умолчание профиля ревью сменилось** — это не раскладка, но проектный текст - под него уже написан. `standard` стал рабочим умолчанием: миграция схемы, - публичный контракт и инвариант ступень больше **не** поднимают, `wide` - означает новое понятие или структурную единицу. Подраздел «Триггеры профиля» - в `docs/review.md` остаётся на месте, но его содержимое надо перечитать. - -**Что переехало:** `docs/tasks/PLAN.md` → `docs/tasks/ROADMAP.md`; достигнутая -цель — из небытия в секцию `Готово`: `close <цель> --implemented` удаляет файл, но -**оставляет строку с датой**. Прежде роадмап отвечал только «что осталось», и -половину его вопроса вели прозой руками. Вместе с -файлом переименован ключ конфига `tasks.plan` → `tasks.roadmap` и токены -команд: `--index plan` → `--index roadmap`, `init --plan-sections` → -`--roadmap-sections`, `init --plan` → `--roadmap`. Старый ключ в -`docs/.pm.json` не игнорируется молча — `tasks.py` останавливается и называет -переименование. - -**Что удалено:** тип `[epic]`. Он был зонтиком между целью и задачами; зонтиком -стала цель, а слишком крупный шаг дробится на шаги помельче под ней. Ноль -употреблений на 97 записей двух живых проектов. - -**Что сделать проекту:** - -1. `git mv docs/tasks/PLAN.md docs/tasks/ROADMAP.md`. -2. Починить ссылки на прежнее имя: `grep -rn 'PLAN\.md' docs/ CLAUDE.md` — - заголовок самого файла («# План» → «# Роадмап»), строка в `docs/tasks/BACKLOG.md`, - упоминания в `docs/passport.md` и в телах задач. -3. `docs/.pm.json`: ключ `tasks.plan`, если он там был, — в `tasks.roadmap`. -4. Проставить род работы живым задачам: `python3 tasks.py check --dir docs/tasks` - перечислит те, у кого его нет. Задним числом весь беклог не переоформляется — - род нужен к взятию, так что порядок такой: сперва то, что берётся в ближайший - спринт, остальное по ходу переоценки. -5. Дописать раздел «Затрагивает» — тем же порядком и по той же причине: сперва - набор спринта, остальное по мере того, как задача попадает в работу. -6. Перечитать «Триггеры профиля» в `docs/review.md`: строки вида «миграция → - `deep`» теперь дублируют умолчание с обратным знаком. Оставить там только то, - что для этого проекта считается **новым понятием** и **правилом - идентичности**, — и убрать остальное, иначе проект возвращает себе прежнюю - частоту полного набора уточнением. -7. Переименовать секции роадмапа: `порядок` → `Запланировано`, `темы` → - `Направления`; завести `Готово` **первой** и `Разработка` последней - (порядок секций поменялся в версии 4 — если едешь сразу на неё, заводи - `Готово` последней и не переставляй дважды). - Прозаические разделы вроде «Что уже пройдено», которые велись руками, - разложить: звенья — строками в `Готово` (дата, слаг, что стало возможно), - обоснование очереди оставить прозой в `Запланировано`. Любой `##` в индексе - проверка считает секцией, и теперь `check` называет чужую секцию ошибкой. -8. Переформулировать цели ответом на **«что приложение будет уметь»**: не - «Работа со слиянием», а «Исход слияния не зависит от порядка доставки». - Свойство поведения — законная цель. Цель, которая не про приложение - (процесс, инструмент), переезжает в `Разработка`. -9. `[epic]`, если он в проекте заводился: это либо цель, либо набор задач под - общей целью. `check` назовёт его неизвестным типом. -10. Прогнать `python3 tasks.py check --dir docs/tasks --fix`: он поднимет - написание канонических секций, поставит отбивку после заголовков и сведёт - секцию в мете файлов с заголовками индексов. Секции беклога проект - переименовывает сам — их имена он выбирал, и трогать их скрипт не вправе. -11. Переписать заголовки задач в форму действия — по мере того, как задача - попадает в работу, а не «заодно»: `check` печатает их число, а `task-form` - предложит формулировки на замену пачкой. -12. Прочитать [language.md](language.md) — и **ничего не переписывать задним - числом**. Правила языка применяются к тому, что пишется и правится сейчас; - сплошная вычитка старых документов стоит дороже, чем даёт. -13. `docs/.pm.json`: `"canon": 3`. - -## Версия 2 — 2026-08-03 - -Шапка записи ADR — мета-блоком общей формы, и у статуса появился объявленный -дом. Раскладка не менялась: правка касается одного шаблона. - -**Что добавилось:** поле `- **Статус:**` в шапке `docs/adr/template.md` — -`заменено на ADR-…` либо `устарело`, у активной записи поля нет. Правило -«старая запись получает статус» было и раньше ([canon.md](canon.md), `adr/`), -но места под него шаблон не отводил: каждая запись изобретала своё, а колонка -«Статус» таблицы `adr/README.md` брала его оттуда, где он у каждого свой. - -**Что переехало:** поля `Дата` и `Источник` в шаблоне стали жирными -(`- **Дата:**`, `- **Источник:**`) — та же форма, что у меты задачи и у записи -журнала дефектов: поле на строку, имя жирным. - -**Что удалено:** ничего. - -**Что сделать проекту:** - -1. Привести `docs/adr/template.md` к скелету версии 2 - ([skeletons.md](skeletons.md), раздел `docs/adr/template.md`). -2. В существующих записях `docs/adr/ADR-*.md`: жирным поля шапки; если статус - записан прозой или заголовком — перенести его полем `- **Статус:**` в шапку - и сверить с колонкой «Статус» таблицы в `docs/adr/README.md`. -3. `docs/.pm.json`: `"canon": 2`. - -## Версия 1 — 2026-08-03 - -Первая версия. Проект любой прежней раскладки приводится к ней скиллом `canon` -в режиме `adopt`, а не `upgrade`. - -**Что вводится:** раскладка целиком — см. [canon.md](canon.md). - -**Что сделать проекту, который приходит из свободной раскладки:** - -1. `docs/.pm.json` с `{"canon": 1}` и путём миграций, если БД есть. -2. Скелет канона целиком; незаполненное — одной честной строкой. -3. `docs/specs/` разобрать: поведение — в `openspec/specs/`, обзор — в - `docs/architecture.md`, знание о чужих системах — в `docs/research/`. - Дубли capability удалить, сверив поимённо. -4. `docs/plan.md` → `docs/tasks/PLAN.md`, шаги плана — целями в «порядок». -5. `BRIEF.md` → `docs/passport.md`. -6. `docs/backlog/` → `docs/tasks/`. -7. `docs/review-journal.md` или `docs/review/journal.md` → `docs/review.md`, - плюс раздел настройки конвейера. -8. `docs/drafts/` растворить: идея → задача `[idea]`, намеренный отказ → ADR, - порядок работ → `PLAN.md`. -9. `docs/review-brief.md`, если заводился, удалить: его разделы разошлись по - документам канона. -10. `conventions.md` → `conventions/`, `local-research.md` → `research/`. -11. Завести `docs/security.md` с периметром первой строкой и `docs/adr/`. -12. В `CLAUDE.md`: severity рядом с каждым инвариантом; семантика гейта (чем - краснеет безусловно, где логи, чего в нём нет и кто тогда гоняет дорогое); - **имя основной ветки**; запреты с путями; где `testdata` и куда писать - временное; **что считается необратимым**; общий станок; ориентир по размеру - спринта. Убрать раздел «Процесс», если он пересказывает пайплайн. -13. В `openspec/config.yaml` оставить только нужды генерации и ссылки. -14. Добавить шаг `docs.py check` в гейт проекта. - -**Копии правил в шаблонах, которые версия 1 уносит в проект** — их правка в -каноне обязана появляться здесь отдельной версией: - -| Что копируется | Дом определения | +| Было | Стало | | --- | --- | -| форма записи журнала дефектов в `docs/review.md` | `av-dev-code/skills/review/references/review-journal.md` | -| правило заведения ADR в `docs/adr/README.md` | [canon.md](canon.md), раздел `adr/` | +| `docs/.docs.json`, ключ `canon` | `.av-dev.toml` в корне, ключ `version` | +| `docs/.docs.json`, ключ `migrations` | `.av-dev.toml`, секция `[docs]` | +| `<каталог задач>/.tasks.json`, ключ `tasks` | тот же `version`: версия теперь одна | +| `<каталог задач>/.tasks.json`, имена частей | `.av-dev.toml`, секция `[tasks]` | + +Корень выбран потому, что он есть у обоих: и у проекта без `docs/`, и у проекта +без каталога задач. Формат TOML — ради комментариев: файл лежит в репозитории +проекта, и назначение числа читают из него самого, а не из документации плагина. + +**Что переехало в вызовах.** Имена скиллов сменили пространство имён и получили +префикс по прежнему плагину: `av-dev-docs:canon` → `av-dev:doc-canon`, +`av-dev-docs:init` → `av-dev:doc-init`, `av-dev-docs:docs` → `av-dev:doc-sync`, +`av-dev-docs:healthcheck` → `av-dev:doc-healthcheck`, `av-dev-tasks:tasks` → +`av-dev:task-track`, `av-dev-tasks:groom` → `av-dev:task-groom`, +`av-dev-code:openspec` → `av-dev:code-openspec`, `av-dev-code:resolve` → +`av-dev:code-resolve`, `av-dev-code:review` → `av-dev:code-review`. + +**Что сделать проекту.** + +1. **Отставшим сперва прежние журналы.** Версия канона в `docs/.docs.json` + меньше 14 — пройди записи до 14 по + [changelog-before-merge.md](changelog-before-merge.md), и только потом эту. + Иначе повышение объявит приведённым то, чего никто не делал. +2. **Завести `.av-dev.toml`** в корне репозитория: `version = 1`, секция + `[docs]` с `migrations`, если ключ был, секция `[tasks]` с `dir` и теми + именами частей, которые в `.tasks.json` отличались от умолчаний. Комментарии + пиши свои — файл читает человек. +3. **Удалить `docs/.docs.json` и `<каталог задач>/.tasks.json`.** Прежние имена + не читаются: два дома для одной версии расходятся молча. Пока старые файлы на + месте, `docs.py check` и `tasks.py check` называют это прежней раскладкой. +4. **Переставить плагины.** `av-dev-docs`, `av-dev-tasks` и `av-dev-code` + удалить, `av-dev` поставить — команды в README репозитория плагинов. +5. **Поправить гейт проекта.** Пути к `docs.py`, `tasks.py` и `openspec.py` + сменились вместе с именами каталогов скиллов: `skills/canon/` → + `skills/doc-canon/`, `skills/tasks/` → `skills/task-track/`, + `skills/openspec/` → `skills/code-openspec/`. Шаг, который не нашёл скрипт, + обязан краснеть, а не пропускаться, — проверь, что он краснеет. +6. **Поправить свои вызовы скиллов** — в `CLAUDE.md`, в `Taskfile`, в записях + задач: короткое имя разрешится в проектную копию, а прежнее полное не + разрешится вовсе. +7. `docs.py check` и `tasks.py check --dir <каталог задач>` — до отсутствия + дрейфа. + +**Чего делать не надо.** Переписывать прошлые записи журналов под новые имена. +Они описывают состояния, которые были, и адрес, верный на день записи, остаётся +верным как свидетельство. diff --git a/av-dev/skills/doc-canon/references/skeletons.md b/av-dev/skills/doc-canon/references/skeletons.md index d1ab2fc..1d540f3 100644 --- a/av-dev/skills/doc-canon/references/skeletons.md +++ b/av-dev/skills/doc-canon/references/skeletons.md @@ -117,7 +117,7 @@ со строкой «запись лежит сжатой и распаковывается целиком». ``` -Нет БД — файла нет, и в `docs/.docs.json` нет ключа `migrations`. +Нет БД — файла нет, и в `.av-dev.toml` нет ключа `[docs] migrations`. ## `docs/security.md` @@ -440,24 +440,32 @@ severity стоит здесь, а не выводится каждым прох говорит (единственный дом, а не форма) — [canon.md](canon.md), раздел `openspec/config.yaml`. -## `docs/.docs.json` +## `.av-dev.toml` -```json -{ - "canon": <текущая версия> -} +```toml +# Раскладка av-dev в этом проекте: версия и настройки проверок. + +version = <текущая версия> + +[docs] +# migrations = "<путь>" — появится, когда появится БД + +[tasks] +dir = "tasks" ``` `<текущая версия>` подставляет `init` или `adopt`, целым числом; берётся она из -`docs.py version` (строка «канон скрипта»), а не из памяти. Литерал здесь -протухает при каждом повышении канона, поэтому его тут и нет: незамещённый -плейсхолдер ломает разбор JSON громко, а отставшее число дало бы дрейф молча. +`docs.py version` (строка «раскладка скрипта»), а не из памяти. Литерал здесь +протухает при каждом повышении, поэтому его тут и нет: незамещённый плейсхолдер +ломает разбор TOML громко, а отставшее число дало бы дрейф молча. -Плюс `"migrations": "<путь>"`, если есть БД. Ключа `"tasks"` здесь **нет**: -настройки каталога задач и версия их формата переехали в свой файл `<каталог -задач>/.tasks.json`, потому что ведёт их другой плагин. Состав ключей — -[canon.md](canon.md). +Комментарии в файле — не украшение, а причина, по которой взят TOML: файл живёт +в чужом репозитории, и назначение числа читают из него самого. Скрипты это +учитывают и правят строку, а не переписывают файл. Состав ключей — +[canon.md](canon.md), раздел `.av-dev.toml`. -Имя файла — по плагину-владельцу, `av-dev-docs`. До версии 13 он звался -`.pm.json`, по распавшемуся `av-dev-pm`; проект с прежним именем `docs.py check` -называет отдельной строкой и зовёт переименовать. +Файл лежит **в корне репозитория**, а не в `docs/`: версия одна на всю +раскладку, и нужна она в том числе проекту, который канон документов ещё не +завёл. Прежние `docs/.docs.json` и `<каталог задач>/.tasks.json` остались от +трёх плагинов, слившихся в один; увидев их, `docs.py check` называет это прежней +раскладкой и зовёт `upgrade`. diff --git a/av-dev/skills/doc-canon/scripts/docs.py b/av-dev/skills/doc-canon/scripts/docs.py index f5e809b..cd74098 100644 --- a/av-dev/skills/doc-canon/scripts/docs.py +++ b/av-dev/skills/doc-canon/scripts/docs.py @@ -17,26 +17,47 @@ from __future__ import annotations import argparse -import json +import importlib.util import re import subprocess import sys from dataclasses import dataclass, field from pathlib import Path +from types import ModuleType from typing import NoReturn -CANON_VERSION = 14 - 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" + +def _load_shared() -> ModuleType: + """Общий читатель `.av-dev.toml` — `shared/config.py` этого же плагина. + + Путь считается от файла скрипта, а не от рабочего каталога: скрипт зовут из + репозитория проекта, где ни плагина, ни его дерева в текущем каталоге нет. + Своё дерево — единственное, куда ходить можно; в чужое не ходим никогда. + """ + path = Path(__file__).resolve().parents[3] / "shared" / "config.py" + spec = importlib.util.spec_from_file_location("avdev_config", path) + if spec is None or spec.loader is None: + print(f"ОТКАЗ: не читается {path} — общий читатель настроек;" + f" переустанови плагин av-dev", file=sys.stderr) + sys.exit(ENV) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +conf = _load_shared() + +# Версия раскладки одна на плагин и живёт в `shared/config.py`: её знают оба +# скрипта, и второе число здесь было бы вторым домом. +CANON_VERSION = conf.VERSION + +# Дом версии и путей, нужных проверкам, — `.av-dev.toml` в корне репозитория. +# До слияния плагинов файлов было два, `docs/.docs.json` и `.tasks.json`, и +# версии двигались порознь; теперь дом один, и лежит он в корне, потому что +# настройки нужны и проекту без `docs/`. +CONFIG = conf.CONFIG_NAME # --- Раскладка канона ------------------------------------------------------- @@ -77,7 +98,7 @@ CONDITIONAL_DOCS = { # Обязательные файлы вне раскладки docs/. REQUIRED = { "CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта", - CONFIG: "версия канона и пути, нужные проверкам", + CONFIG: "версия раскладки av-dev и пути, нужные проверкам", } # Файлы, которые документ-каталог обязан держать сверх README.md. @@ -86,9 +107,10 @@ DOC_EXTRA = { } # Служебное в docs/ и каталог задач, оставшийся там от прежней раскладки. Формы -# у них скрипт не проверяет, и по разным причинам: `.docs.json` не markdown, а -# задачи **принадлежат другому плагину** — `av-dev-tasks`, со своим скриптом, -# своим конфигом и своей версией формата (её сторожит `tasks.py check`). +# у них скрипт не проверяет, и по разным причинам: `.docs.json` не markdown (и +# сам он теперь след прежней раскладки, о котором говорит `check_required`), а +# задачи ведёт **другой скилл** — `task-track`, со своим скриптом и своими +# проверками. # # Дом задач с версии 11 — `tasks/` в корне репозитория, то есть вне `docs/` # вовсе. `docs/tasks/` здесь терпится потому, что непереехавший проект не должен @@ -106,11 +128,11 @@ NOT_DOCS = {".docs.json", ".pm.json", "tasks"} RETIRED = { "review-brief.md": "документы канона и есть бриф; остаток — в review", "review-journal.md": "→ документ review", - "plan.md": "→ tasks/ROADMAP.md (плагин av-dev-tasks)", + "plan.md": "→ tasks/ROADMAP.md (ведёт скилл task-track)", "local-research.md": "→ документ research", "specs": "поведение → openspec/specs/, обзор → тема architecture", "drafts": "идея → запись research, отказ → ADR, порядок → ROADMAP.md", - "backlog": "→ tasks/ в корне репозитория (плагин av-dev-tasks)", + "backlog": "→ tasks/ в корне репозитория (ведёт скилл task-track)", } # --- Слаги в именах файлов -------------------------------------------------- @@ -264,16 +286,15 @@ def fail(code: int, msg: str) -> NoReturn: def read_config(root: Path, rep: Report) -> dict: - path = root / CONFIG - if not path.exists(): - return {} + """Настройки проекта целиком; проверкам канона нужна секция `[docs]`.""" try: - data = json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exc: - fail(ENV, f"{CONFIG} не разбирается: {exc}") - if not isinstance(data, dict): - fail(ENV, f"{CONFIG} должен быть объектом") - return data + return conf.read(root) + except conf.ConfigError as exc: + fail(ENV, str(exc)) + + +def docs_cfg(cfg: dict) -> dict: + return conf.section(cfg, "docs") # --- Проверки --------------------------------------------------------------- @@ -282,22 +303,19 @@ def read_config(root: Path, rep: Report) -> dict: def check_version(root: Path, cfg: dict, rep: Report) -> None: if not (root / CONFIG).exists(): return # об отсутствии файла скажет check_required, второй раз не нужно - if "canon" not in cfg: - rep.error(f"в {CONFIG} нет ключа canon — версия канона не объявлена") - return - got = cfg["canon"] - if not isinstance(got, int): - rep.error(f"canon в {CONFIG} должен быть целым числом, а не {got!r}") + got = conf.version(cfg) + if got is None: + rep.error(f"в {CONFIG} нет ключа version — версия раскладки не объявлена") return if got < CANON_VERSION: rep.error( - f"проект приведён к канону версии {got}, текущая — {CANON_VERSION}: " + f"проект приведён к раскладке версии {got}, текущая — {CANON_VERSION}: " f"нужен canon upgrade" ) elif got > CANON_VERSION: rep.error( - f"проект приведён к канону версии {got}, а скрипт знает {CANON_VERSION}: " - f"устарел плагин, обнови маркетплейс" + f"проект приведён к раскладке версии {got}, а скрипт знает" + f" {CANON_VERSION}: устарел плагин, обнови маркетплейс" ) @@ -331,15 +349,17 @@ def check_required(root: Path, cfg: dict, rep: Report) -> None: for rel, what in REQUIRED.items(): if (root / rel).exists(): continue - # Файл под прежним именем — это не «нет файла», а незаконченный переезд, - # и чинится он одной командой. Без этой ветки проект услышал бы «нет - # версии канона» и пошёл заводить второй файл рядом с первым. - if rel == CONFIG and (root / LEGACY_CONFIG).exists(): + # Настройки под прежними именами — это не «нет файла», а незаконченный + # переезд. Без этой ветки проект слышал бы «нет версии» и шёл заводить + # второй файл рядом с первым, а старые остались бы вторым домом. + legacy = conf.legacy_files(root) + if rel == CONFIG and legacy: rep.error( - f"нет {rel} — {what}. Настройки лежат под прежним именем" - f" {LEGACY_CONFIG} (от плагина av-dev-pm, которого больше нет):" - f" `git mv {LEGACY_CONFIG} {rel}` — журнал канона, версия 13." - f" Прежнее имя не читается, поэтому в этом прогоне всё" + f"нет {rel} — {what}. Настройки лежат по прежней раскладке" + f" ({', '.join(legacy)}) — она осталась от трёх плагинов, которые" + f" слились в один: перенеси значения и удали старые файлы" + f" операцией upgrade скилла av-dev:doc-canon (журнал, версия 1)." + f" Прежние имена не читаются, поэтому в этом прогоне всё" f" остальное проверено так, будто настроек нет вовсе" ) continue @@ -360,18 +380,20 @@ def check_required(root: Path, cfg: dict, rep: Report) -> None: if not (home / extra).is_file(): rep.error(f"нет docs/{name}/{extra} — {why}") + docs = docs_cfg(cfg) for name, (key, kind, what) in CONDITIONAL_DOCS.items(): home, complaint = doc_home(root, name) if complaint: rep.error(complaint) - if key in cfg and home is None: + if key in docs and home is None: rep.error( f"нет документа {name} (docs/{name}.md или docs/{name}/)," f" категория «{kind}» — {what}" - f" (обязателен: в .docs.json объявлен {key})" + f" (обязателен: в {CONFIG} объявлен [docs] {key})" ) - elif key not in cfg and home is None: - rep.skip(f"{name} — в .docs.json нет ключа {key}, проверка неприменима") + elif key not in docs and home is None: + rep.skip(f"{name} — в {CONFIG} нет ключа [docs] {key}," + f" проверка неприменима") def check_stray(root: Path, rep: Report) -> None: @@ -551,9 +573,10 @@ 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") + migrations = docs_cfg(cfg).get("migrations") if not migrations: - rep.skip("в .docs.json нет ключа migrations — сверка со схемой неприменима") + rep.skip(f"в {CONFIG} нет ключа [docs] migrations —" + f" сверка со схемой неприменима") return if not base: rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась") @@ -630,9 +653,9 @@ def cmd_check(args: argparse.Namespace) -> int: def cmd_version(args: argparse.Namespace) -> int: root = Path(args.dir).resolve() cfg = read_config(root, Report()) - got = cfg.get("canon", "не объявлена") - print(f"канон скрипта: {CANON_VERSION}") - print(f"канон проекта: {got}") + got = conf.version(cfg) + print(f"раскладка скрипта: {CANON_VERSION}") + print(f"раскладка проекта: {got if got is not None else 'не объявлена'}") return OK diff --git a/av-dev/skills/doc-init/SKILL.md b/av-dev/skills/doc-init/SKILL.md index 2705d99..e80369d 100644 --- a/av-dev/skills/doc-init/SKILL.md +++ b/av-dev/skills/doc-init/SKILL.md @@ -26,7 +26,7 @@ description: "Завести новый проект — сессия вопро | `passport.md` | `architecture.md` | | `CLAUDE.md` | `database.md` | | `security.md` | `conventions/` | -| `docs/.docs.json` | `research/`, `adr/` | +| `.av-dev.toml` | `research/`, `adr/` | | | `review.md` — журнал пуст, настройка появится с первым ревью | Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет, @@ -125,7 +125,7 @@ description: "Завести новый проект — сессия вопро **Вызов не разрешился** — проект без конвейера живёт без OpenSpec законно: строка доклада, и дальше; `docs.py check` о каталоге тоже промолчит. -4. Заведи `docs/.docs.json` с текущей версией канона — число берётся из +4. Заведи `.av-dev.toml` в корне с текущей версией раскладки — число берётся из `docs.py version`, а не из памяти. 5. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и отдельным файлом не остаётся: два дома для одного замысла разойдутся на diff --git a/av-dev/skills/task-track/SKILL.md b/av-dev/skills/task-track/SKILL.md index 0304f52..31d0edb 100644 --- a/av-dev/skills/task-track/SKILL.md +++ b/av-dev/skills/task-track/SKILL.md @@ -407,7 +407,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап | 0 | сошлось / сделано | дальше по сценарию | | 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать | | 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу | -| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `<каталог задач>/.tasks.json`, повтор не поможет | +| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `.av-dev.toml` в корне, повтор не поможет | | 4 | внутренний сбой | дефект скрипта, доложить | Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога @@ -494,38 +494,24 @@ 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`: отстало — строка расхождения, ушло вперёд — устарел плагин. +Формат каталога задач меняется, и проект должен знать, к какой версии он +приведён. Число живёт ключом `version` в `.av-dev.toml` в корне репозитория, +журнал версий — [журнал скилла `doc-canon`](../doc-canon/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` и вправе сказать «позови этот скилл», но не двигать версию -формата задач: две версии, ходящие по одному журналу, разъедутся на первом же -проекте, где стоит один плагин без другого. +**Повышает проект скилл `av-dev:doc-canon`, операция `upgrade`** — он идёт по +журналу, двигает число и зовёт этот скилл там, где запись касается задач. Здесь +повышения нет намеренно: две операции, двигающие одно число, разъезжаются на +первом же проекте, где прошла только одна из них. ## Сценарии @@ -689,19 +675,17 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап действительно новый, а перевод чужой раскладки делает `av-dev:doc-canon`. У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и полагаться на него скилл не должен: молча найденный чужой каталог это дрейф. -- **Версия формата и настройки живут в `<каталог задач>/.tasks.json`** — свой - файл у своего плагина: ключ `tasks` с версией формата плюс **имена** файлов и - заголовков, и последние — только если отличаются от умолчания. Неизвестный - ключ — код 3 на любой команде, так что лишнее слово в этом объекте - останавливает работу с задачами целиком. +- **Версия и настройки живут в `.av-dev.toml` в корне репозитория** — версия + ключом `version`, настройки каталога секцией `[tasks]`: `dir` — где каталог + лежит, плюс **имена** файлов и заголовков, и последние только если отличаются + от умолчания. Неизвестный ключ в секции — код 3 на любой команде, так что + лишнее слово останавливает работу с задачами целиком. - Дом именно свой, а не `docs/.docs.json`, потому что `docs/` принадлежит - плагину канона: проект, поставивший учёт работ без него, каталога `docs/` не - имеет вовсе. Прежний ключ `tasks` в `docs/.pm.json` читается, **только когда - своего файла нет** — для проектов, заведённых до раскола плагинов; скрипт при - этом говорит замечанием, куда его перенести. Есть оба — побеждает свой, и об - этом тоже говорится вслух: молча выбранный из двух конфиг это дрейф. Версию - прежний дом не знает и знать не может — она читается только из своего файла. + Дом в корне, а не внутри каталога задач, по двум причинам: настройка, лежащая + внутри настраиваемого каталога, не смогла бы сказать, **где он**; и версия + одна на весь плагин, а корень есть и у проекта без `docs/`. Прежние + `<каталог задач>/.tasks.json` и `docs/.docs.json` не читаются — увидев их, + скрипт говорит «прежняя раскладка» и зовёт `upgrade`. - **Секции беклога** берутся из заголовков `##` индекса как есть; их количество и названия — дело проекта (умолчание `Ядро` / `Инфра`). **В конфиге их нет** — второй список разошёлся бы с заголовками молча. diff --git a/av-dev/skills/task-track/references/adopt.md b/av-dev/skills/task-track/references/adopt.md index 8773845..fe94429 100644 --- a/av-dev/skills/task-track/references/adopt.md +++ b/av-dev/skills/task-track/references/adopt.md @@ -66,7 +66,7 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \ всегда `tasks`. Секции беклога (`--sections`) — по умолчанию `Ядро,Инфра`; если у проекта деление другое по существу, оно называется здесь, а не подгоняется под умолчание, и становится **заголовками `##` - индекса** — их единственным домом. В `.tasks.json` секции не пишутся: там + индекса** — их единственным домом. В `.av-dev.toml` секции не пишутся: там версия формата и имена частей, а второй список секций разошёлся бы с заголовками молча. 2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два diff --git a/av-dev/skills/task-track/scripts/tasks.py b/av-dev/skills/task-track/scripts/tasks.py index 9057d91..8ac9239 100755 --- a/av-dev/skills/task-track/scripts/tasks.py +++ b/av-dev/skills/task-track/scripts/tasks.py @@ -7,11 +7,11 @@ ровно в одном из них за раз. `REJECTED.md` индексом не считается: он не говорит, где запись числится, он кладбище ушедшего. -Раскладка. Путь каталога — `tasks/` в корне репозитория, жёстко. Каталог -принадлежит этому плагину, а не канону документов: `docs/` ведёт другой плагин, и -проект, поставивший учёт работ без него, каталога `docs/` не имеет вовсе. Имена -внутри и **версия формата** живут в `tasks/.tasks.json`; журнал версий — -references/changelog.md рядом со скриптом. +Раскладка. Путь каталога — `tasks/` в корне репозитория по умолчанию; другой +называется ключом `[tasks] dir`. Каталог принадлежит этому скиллу, а не канону +документов: учёт работ ведут и в проекте, который к канону не приведён. Имена +частей и **версия раскладки** живут в `.av-dev.toml` в корне; журнал версий — +references/changelog.md скилла doc-canon. tasks/ items/ задачи и цели файлами, .md @@ -102,32 +102,47 @@ goal | feature | fix | chore | research, по-английски, как и пр import argparse import datetime +import importlib.util import json import re import subprocess import sys from pathlib import Path +from types import ModuleType -CONFIG_NAME = ".tasks.json" # дом настроек и версии: свой файл в каталоге -PM_CONFIG_REL = "../.pm.json" # прежний дом настроек: docs/.pm.json, ключ "tasks" -# Версия формата задач — **своя, а не канона документов**. Число живёт ключом -# `tasks` в `.tasks.json`, журнал версий — references/changelog.md рядом со -# скриптом, повышает его операция `upgrade` скилла `av-dev:task-track`. +def _load_shared() -> ModuleType: + """Общий читатель `.av-dev.toml` — `shared/config.py` этого же плагина. + + Путь считается от файла скрипта: зовут его из репозитория проекта, где + дерева плагина в текущем каталоге нет. + """ + path = Path(__file__).resolve().parents[3] / "shared" / "config.py" + spec = importlib.util.spec_from_file_location("avdev_config", path) + if spec is None or spec.loader is None: + print(f"ОТКАЗ: не читается {path} — общий читатель настроек;" + f" переустанови плагин av-dev", file=sys.stderr) + sys.exit(3) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +conf = _load_shared() + +CONFIG_NAME = conf.CONFIG_NAME # дом настроек и версии: `.av-dev.toml` в корне + +# Версия раскладки — **одна на плагин**, и живёт она в `shared/config.py`. +# Своей у каталога задач больше нет: пока плагинов было три и ставились они +# порознь, проект мог иметь учёт работ без канона документов, и общее число +# было бы домом, которого у половины проектов нет. Плагин один — довод ушёл, а +# два числа вместо одного оставляли бы вопрос «по какому журналу повышать». # -# Число именно своё, потому что плагин ставится в одиночку: проект, взявший учёт -# работ без канона документов, каталога `docs/` не имеет вовсе, а значит не имеет -# и версии канона — сверять было бы не с чем. Копия чужого числа в этом скрипте -# была бы вторым домом для одной версии и разъехалась бы молча при обновлении -# одного плагина без другого. -# -# Переезды каталога задач, случившиеся до появления этого числа (в корень — -# канон 11, отмена спринтов — канон 12), задним числом сюда не переписаны: они -# уже названы журналом канона, и второй перечень тех же шагов разошёлся бы с -# первым. Версия 1 — формат на день её появления, что бы проекту ни пришлось -# пройти до неё. -FORMAT_VERSION = 1 -VERSION_KEY = "tasks" +# Переезды каталога, случившиеся до слияния (в корень, отмена спринтов), задним +# числом в журнал не переписаны: они названы прежними журналами, и второй +# перечень тех же шагов разошёлся бы с первым. +FORMAT_VERSION = conf.VERSION +VERSION_KEY = conf.VERSION_KEY EXIT_OK = 0 EXIT_DRIFT = 1 @@ -135,6 +150,11 @@ EXIT_USAGE = 2 EXIT_ENV = 3 EXIT_INTERNAL = 4 +# Ключ `dir` в DEFAULTS не входит намеренно: он говорит, **где** каталог, а не +# как названы его части, и в `Layout` (тот про имена внутри) ему делать нечего. +DIR_KEY = "dir" +DEFAULT_DIR = "tasks" + DEFAULTS = { "items": "items", "backlog": "BACKLOG.md", @@ -435,9 +455,14 @@ class Layout: """Каталог задач и имена его частей. Всё настраивается: у соседнего проекта может быть другой подкаталог и другие имена индексов, а семантика та же.""" - def __init__(self, root: Path, cfg: dict): + def __init__(self, root: Path, cfg: dict, project: Path | None = None, + full: dict | None = None): self.root = root - self.cfg = {**DEFAULTS, **cfg} + # Корень проекта — там, где лежит `.av-dev.toml`. Он нужен отдельно от + # каталога задач: версия объявлена в корне, а имена частей — внутри. + self.project = project or root + self.full = full or {} + self.cfg = {**DEFAULTS, **{k: v for k, v in cfg.items() if k != DIR_KEY}} self.items = root / self.cfg["items"] def index(self, kind: str) -> Path: @@ -451,64 +476,28 @@ class Layout: return ("backlog", "roadmap") -def load_config(root: Path) -> dict: - """Настройки каталога задач и версия его формата. +def load_config(project: Path) -> dict: + """Весь `.av-dev.toml` проекта. Секция задач берётся из него отдельно. - Дом — `<каталог задач>/.tasks.json`: **свой файл у своего плагина**. Ключ - `tasks` в `docs/.pm.json` читается, пока живы проекты, заведённые до раскола - плагинов, и только когда своего файла нет; когда есть оба, побеждает свой, и - об этом говорится вслух — молча выбранный из двух конфиг это дрейф, который - потом никто не объяснит. - - Порядок именно такой, а не наоборот, потому что `docs/` принадлежит другому - плагину. Проект, поставивший учёт задач без канона документов, каталога - `docs/` не имеет вовсе, и дом настроек, лежащий в чужом дереве, был бы домом, - которого у половины проектов нет. - - Версия формата (ключ `tasks`) читается **только из своего файла**: прежний - дом её не знал и знать не может, и молча выведенная из его отсутствия версия - была бы догадкой о том, что чинится одной строкой. + Дом настроек — **корень репозитория**, а не каталог задач: файл держит + версию раскладки, которая одна на плагин, и ключ `[tasks] dir`, который + говорит, где каталог лежит. Настройка внутри настраиваемого каталога не + смогла бы сказать, где он. """ - path = root / CONFIG_NAME - pm = (root / PM_CONFIG_REL).resolve() - if path.is_file(): - # Чужой конфиг здесь только повод для замечания, поэтому его поломка не - # наша: битый `docs/.pm.json` не должен ронять задачи, у которых свой - # файл на месте и читается. - try: - stale = pm.is_file() and isinstance(_read_json(pm).get("tasks"), dict) - except Env: - stale = False - if stale: - print(f"ЗАМЕЧАНИЕ настройки взяты из {path}; ключ «tasks» в {pm}" - f" остался от прежней раскладки и не читается — убери его", - file=sys.stderr) - return _validate_config(_read_json(path), path) - if pm.is_file(): - data = _read_json(pm) - section = data.get("tasks", {}) - if not isinstance(section, dict): - raise Env(f"{pm}: ключ «tasks» — ожидался объект с настройками") - if section: - print(f"ЗАМЕЧАНИЕ настройки взяты из ключа «tasks» в {pm} — это" - f" прежний дом. Перенеси их в {path}: каталог docs/ ведёт" - f" другой плагин, и его может не быть", file=sys.stderr) - return _validate_config(section, pm) - return {} - - -def _read_json(path: Path) -> dict: try: - data = json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError as e: - raise Env(f"{path}: не разбирается как JSON — {e}") from e - if not isinstance(data, dict): - raise Env(f"{path}: ожидался объект с настройками") + data = conf.read(project) + except conf.ConfigError as e: + raise Env(str(e)) from e + _validate_config(conf.section(data, "tasks"), project / CONFIG_NAME) return data +def tasks_section(full: dict) -> dict: + return conf.section(full, "tasks") + + def _validate_config(data: dict, path: Path) -> dict: - unknown = set(data) - set(DEFAULTS) - {VERSION_KEY} + unknown = set(data) - set(DEFAULTS) - {DIR_KEY} # Ключ «plan» был домом оглавления целей до того, как файл стал ROADMAP.md. # Без этой ветки проект со старым конфигом получал бы «неизвестный ключ» и # искал опечатку там, где на самом деле переименование канона. @@ -518,19 +507,12 @@ def _validate_config(data: dict, path: Path) -> dict: f" av-dev:doc-canon (upgrade), а не правь ключ в одиночку:" f" файл и ссылки на него переезжают вместе с ним") if unknown: - known = sorted({*DEFAULTS, VERSION_KEY}) - raise Env(f"{path}: неизвестные ключи: {', '.join(sorted(unknown))}" - 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}") + known = sorted({*DEFAULTS, DIR_KEY}) + raise Env(f"{path}: неизвестные ключи в секции [tasks]:" + f" {', '.join(sorted(unknown))} (известны: {', '.join(known)})") + # Версия раскладки лежит ключом верхнего уровня и проверяется общим + # читателем: здесь судится только секция задач, и все её ключи — строки. 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): @@ -538,17 +520,15 @@ def _validate_config(data: dict, path: Path) -> dict: return data -def config_home(root: Path) -> Path | None: +def config_home(lay: Layout) -> Path | None: """Откуда настройки читаются на самом деле — и куда, значит, слать чинить. - Порядок тот же, что в `load_config`: свой `.tasks.json` побеждает. Без этой - функции сообщения об ошибке звали бы править файл, который не читается. + Дом один — `.av-dev.toml` в корне проекта; None значит «файла нет, работаем + на умолчаниях». Без этой функции сообщения об ошибке звали бы править файл, + которого нет. """ - path = root / CONFIG_NAME - if path.is_file(): - return path - pm = (root / PM_CONFIG_REL).resolve() - return pm if pm.is_file() else None + path = lay.project / CONFIG_NAME + return path if path.is_file() else None def config_problems(lay: Layout) -> list[str]: @@ -558,7 +538,7 @@ def config_problems(lay: Layout) -> list[str]: check обвинять невиновных: «ссылка на несуществующий файл», хотя файл на месте, а мимо смотрит конфиг. """ - where = str(config_home(lay.root) or "умолчания (конфига нет)") + where = str(config_home(lay) or "умолчания (конфига нет)") out = [] if not lay.items.is_dir(): out.append(f"{where}: items = «{lay.cfg['items']}» → {lay.items} — каталога нет") @@ -582,36 +562,43 @@ def version_problems(lay: Layout) -> list[str]: бы объявить каталог приведённым к формату, шагов которого никто не делал. Заводит число `init`, двигает — операция `upgrade` скилла. """ - path = lay.root / CONFIG_NAME - # Прежний дом (`docs/.pm.json`) версии не знает, поэтому спрашиваем строго - # свой файл: «конфиг нашёлся» и «версия объявлена» это разные события. + path = lay.project / CONFIG_NAME if not path.is_file(): - return [f"нет {path} — версия формата задач не объявлена." - f" Заведи файл с «{VERSION_KEY}»: {FORMAT_VERSION} (журнал версий —" - f" references/changelog.md скилла av-dev:task-track)"] - # Что число целое, уже проверил `_validate_config` — иначе сюда не дошли бы + legacy = conf.legacy_files(lay.project, lay.root) + if legacy: + return [f"нет {path}, а прежняя раскладка на месте" + f" ({', '.join(legacy)}): перенеси настройки и удали старые" + f" файлы операцией upgrade скилла av-dev:doc-canon"] + return [f"нет {path} — версия раскладки не объявлена." + f" Заведи файл с «{VERSION_KEY} = {FORMAT_VERSION}» (журнал" + f" версий — references/changelog.md скилла av-dev:doc-canon)"] + # Что число целое, уже проверил общий читатель — иначе сюда не дошли бы # вовсе (код 3). Здесь `isinstance` значит ровно «ключ есть». - got = lay.cfg.get(VERSION_KEY) - if not isinstance(got, int): - return [f"{path}: нет ключа «{VERSION_KEY}» — версия формата не объявлена," - f" текущая {FORMAT_VERSION}"] + got = conf.version(lay.full) + if got is None: + return [f"{path}: нет ключа «{VERSION_KEY}» — версия раскладки не" + f" объявлена, текущая {FORMAT_VERSION}"] if got < FORMAT_VERSION: - return [f"каталог приведён к формату версии {got}, текущая —" + return [f"проект приведён к раскладке версии {got}, текущая —" f" {FORMAT_VERSION}: нужно повышение по журналу" - f" (скилл av-dev:task-track, операция upgrade)"] + f" (скилл av-dev:doc-canon, операция upgrade)"] if got > FORMAT_VERSION: - return [f"каталог приведён к формату версии {got}, а скрипт знает" + return [f"проект приведён к раскладке версии {got}, а скрипт знает" f" {FORMAT_VERSION}: устарел плагин, обнови маркетплейс"] return [] -def looks_like_tasks(p: Path) -> bool: - if (p / CONFIG_NAME).is_file(): - return True - try: # индекс мог быть переименован через конфиг - name = load_config(p).get("backlog", DEFAULTS["backlog"]) - except Env: - name = DEFAULTS["backlog"] +def looks_like_tasks(p: Path, names: dict | None = None) -> bool: + """Каталог задач узнаётся индексом, а не служебным файлом. + + Служебный файл теперь лежит в корне проекта и о каталоге говорит ключом + `[tasks] dir`; узнавать каталог по нему значило бы объявить его задачами + ровно там, куда указывает ключ, — даже если по этому пути пусто. + + Имя индекса берётся из настроек: проект вправе назвать его по-своему, и + поиск по умолчанию не нашёл бы переименованного каталога вовсе. + """ + name = (names or {}).get("backlog") or DEFAULTS["backlog"] return (p / name).is_file() @@ -619,33 +606,51 @@ def resolve_layout(explicit: str | None) -> Layout: """Каталог задач для команд, кроме init. Цепочка разрешения: явный `--dir` (обязан быть внутри рабочего каталога) → - `.tasks.json` или умолчания вверх от текущего каталога. Указатель в - `CLAUDE.md` проекта — звено между ними, но читает его агент и передаёт - сюда `--dir`: скрипт не разбирает чужую документацию. + ключ `[tasks] dir` из `.av-dev.toml` в корне → умолчание `tasks/` вверх от + текущего каталога. Указатель в `CLAUDE.md` проекта — звено между первым и + вторым, но читает его агент и передаёт сюда `--dir`: скрипт не разбирает + чужую документацию. """ + here = Path.cwd().resolve() + project = conf.find_root(here) + full = load_config(project) if project else {} + names = tasks_section(full) + if explicit: root = Path(explicit) if not dir_within_cwd(root): raise Env(f"--dir вне рабочего каталога: {explicit}") - if not looks_like_tasks(root): + if not looks_like_tasks(root, names): raise Env(f"задач нет в «{explicit}»;" f" новый проект — tasks.py init --dir {explicit}") - return Layout(root, load_config(root)) - here = Path.cwd().resolve() + return Layout(root, names, project or root.resolve(), full) + + if project: + candidate = project / names.get(DIR_KEY, DEFAULT_DIR) + if looks_like_tasks(candidate, names): + return Layout(relative_if_inside(candidate, here), names, project, full) + + # Проект без `.av-dev.toml` — учёт работ ведут и до того, как канон заведён. + # Тогда каталог ищется умолчанием вверх, а версия объявится на `adopt`. for base in (here, *here.parents): - for candidate in (base, base / "tasks", base / "docs/tasks", base / "doc/tasks"): - if looks_like_tasks(candidate): - try: - rel = candidate.relative_to(here) - except ValueError: - rel = candidate - return Layout(rel if str(rel) != "." else candidate, load_config(candidate)) + for candidate in (base, base / DEFAULT_DIR, base / "docs/tasks", base / "doc/tasks"): + if looks_like_tasks(candidate, names): + return Layout(relative_if_inside(candidate, here), names, + project or base, full) if (base / ".git").exists(): break # выше корня репозитория не ищем - raise Env("каталог задач не найден: ни --dir, ни tasks/ вверх от" - f" {here}. Путь всегда tasks/ в корне репозитория; прежний" - " docs/tasks переезжает по записи 11 журнала версий канона," - " новый проект — tasks.py init --dir tasks") + raise Env(f"каталог задач не найден: ни --dir, ни ключ [tasks] {DIR_KEY} в" + f" {CONFIG_NAME}, ни {DEFAULT_DIR}/ вверх от {here}." + f" Новый проект — tasks.py init --dir {DEFAULT_DIR}") + + +def relative_if_inside(path: Path, here: Path) -> Path: + """Путь покороче для сообщений, если каталог лежит под текущим.""" + try: + rel = path.relative_to(here) + except ValueError: + return path + return path if str(rel) == "." else rel # --- Чтение индексов --- @@ -1079,7 +1084,7 @@ def check(lay: Layout, fix: bool = False) -> int: for p in problems: print(f"КОНФИГ {p}") print("\nсперва конфиг: пока он мимо, всё остальное диагностируется ложно" - f" (правь {config_home(lay.root) or lay.root / CONFIG_NAME}" + f" (правь {config_home(lay) or lay.project / CONFIG_NAME}" f" или переименуй файлы)") return EXIT_ENV @@ -2596,15 +2601,14 @@ 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] = {} - # Файл заводится всегда, даже когда все имена умолчательные: в нём живёт - # версия формата, а версия — не настройка, от которой можно отказаться. - # - # Пишем всегда в свой `.tasks.json`, даже когда рядом живёт `docs/.pm.json`: - # дом настроек принадлежит этому плагину, а `docs/` — другому, и его в - # проекте может не быть. load_config читает свой файл первым, так что - # записанное сюда и прочитается отсюда. - out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False, - indent=2) + "\n" + # Служебный файл заводится всегда, даже когда все имена умолчательные: в нём + # живёт версия раскладки, а версия — не настройка, от которой можно + # отказаться. Файл уже есть (проект под каноном, заводят только задачи) — + # он не перезаписывается: комментарии в нём принадлежат человеку. Тогда + # недостающие ключи секции дописываются построчно, и делает это `cmd_init` + # после записи файлов, потому что правка идёт по живому файлу, а не планом. + if not (lay.project / CONFIG_NAME).is_file(): + out[lay.project / CONFIG_NAME] = conf.skeleton(FORMAT_VERSION, tasks=cfg) out[lay.index("backlog")] = ( "# Беклог\n\n" f"Что **можно взять**. Одна задача = один файл `{lay.cfg['items']}/.md`\n" @@ -2670,8 +2674,18 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int: # Имена частей — следом и только те, что названы явно: умолчание, записанное # в файл, стало бы вторым домом для того же имени. В раскладку версия не # идёт — `Layout` про имена, и число среди имён там ничего не значит. - cfg = {VERSION_KEY: FORMAT_VERSION, **names} - lay = Layout(root, names) + # Каталог задач называется ключом `dir`, если он не умолчательный: без него + # `.av-dev.toml` не сможет сказать, где искать, и разрешение уедет на + # умолчание — молча и в другой каталог. + project = conf.find_root() or Path.cwd().resolve() + cfg = dict(names) + try: + rel = root.resolve().relative_to(project).as_posix() + except ValueError: + raise Usage(f"каталог задач {root} вне проекта {project}") from None + if rel != DEFAULT_DIR: + cfg[DIR_KEY] = rel + lay = Layout(root, names, project, load_config(project)) if lay.index("backlog").exists(): raise Usage(f"{lay.index('backlog')} уже есть — каталог задач заведён") @@ -2692,12 +2706,13 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int: for path, text in init_files(lay, sections, roadmap_sections, cfg).items(): plan.file(path, text) plan.commit() + added = conf.merge_section(project, "tasks", cfg) if cfg else [] print(f"каталог задач заведён: {root}") print(f" секции беклога: {', '.join(sections)};" f" секции роадмапа канонические: {', '.join(roadmap_sections)}") - what = ("версия формата и имена частей записаны" if names - else "версия формата записана") - print(f" {what} в {root / CONFIG_NAME}: формат {FORMAT_VERSION}") + if added: + print(f" дописано в [tasks] {project / CONFIG_NAME}: {', '.join(added)}") + print(f" версия раскладки в {project / CONFIG_NAME}: {FORMAT_VERSION}") return EXIT_OK @@ -2999,7 +3014,8 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int: root = Path(pl["target"]) if not dir_within_cwd(root): raise Usage(f"target вне рабочего каталога: {root}") - lay = Layout(root, {}) + project = conf.find_root() or Path.cwd().resolve() + lay = Layout(root, {}, project, load_config(project)) sections = pl.get("sections_backlog") or uniq_sections(DEFAULT_SECTIONS) roadmap_sections = pl.get("sections_roadmap") or uniq_sections(DEFAULT_ROADMAP_SECTIONS) known_sections = {s.lower() for s in sections} @@ -3058,8 +3074,7 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int: # Версия та же, что у `init`: каталог выводится из чужой раскладки сегодня и # сегодняшним форматом, сколько бы лет ни было тому, из чего он выведен. # Имён частей здесь нет — адаптация раскладывает всё по умолчаниям. - for path, text in init_files(lay, sections, roadmap_sections, - {VERSION_KEY: FORMAT_VERSION}).items(): + for path, text in init_files(lay, sections, roadmap_sections, {}).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 9c3ec1a..f4ca068 100644 --- a/scripts/addresses.py +++ b/scripts/addresses.py @@ -56,8 +56,11 @@ OWNERS = { # Журналы: описывают прошлые состояния и задним числом не переписываются. # Адрес, верный на момент записи, здесь останется навсегда, и это не дрейф. JOURNALS = { - "av-dev/skills/doc-canon/references/changelog.md": "журнал версий канона", - "av-dev/skills/task-track/references/changelog.md": "журнал версий формата задач", + "av-dev/skills/doc-canon/references/changelog.md": "журнал версий раскладки", + "av-dev/skills/doc-canon/references/changelog-before-merge.md": + "журнал версий канона до слияния", + "av-dev/skills/doc-canon/references/changelog-tasks-before-merge.md": + "журнал версий формата задач до слияния", "DECISIONS.md": "журнал решений", "HISTORY.md": "журнал работ", "NOTES.md": "рабочие заметки",