конфиг: одна версия и один служебный файл, .av-dev.toml в корне
Версий было две — канон 14 в docs/.docs.json и формат задач 1 в <каталог задач>/.tasks.json, — и порознь они двигались потому, что плагины ставились порознь. Плагин один, версия одна и начинается с 1; журналы обеих прежних нумераций закрыты и лежат рядом непереписанными, действующий журнал открывается записью о слиянии с перечнем шагов проекту. Формат TOML взят ради комментариев: файл живёт в репозитории проекта, и назначение числа читают из него самого. Отсюда правило записи — скрипты правят строку, а не переписывают файл. Читатель общий, shared/config.py: два разбора одной схемы были бы двумя домами. Каталог задач перестал узнаваться служебным файлом и называется ключом [tasks] dir; узнают его по индексу. Прежние файлы не читаются — увидев их, docs.py и tasks.py называют прежнюю раскладку и зовут upgrade.
This commit is contained in:
@@ -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 — не проверено
|
||||
|
||||
@@ -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` — единственное исключение: служебный файл, не документ, в плане
|
||||
не упоминается.
|
||||
|
||||
**Категории `источник` и `процессный` закрыты — они перечислены выше поимённо.**
|
||||
|
||||
@@ -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"
|
||||
@@ -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.*`.** В ней лежит
|
||||
настройка самого конвейера: вопросы по темам, журнал дефектов, типовые узлы,
|
||||
|
||||
@@ -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`), а ручной проход по нескольким записям
|
||||
подряд — ровно то место, где половина шага делается и забывается. Судьи и есть
|
||||
|
||||
@@ -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`
|
||||
игнорирует, отсутствующий — считает «проверка неприменима» и говорит об этом
|
||||
|
||||
@@ -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/` |
|
||||
+7
-16
@@ -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`.
|
||||
Запись ниже не переписана под нынешние имена: она описывает состояние, которое
|
||||
было.
|
||||
|
||||
---
|
||||
|
||||
@@ -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 <каталог задач>` — до отсутствия
|
||||
дрейфа.
|
||||
|
||||
**Чего делать не надо.** Переписывать прошлые записи журналов под новые имена.
|
||||
Они описывают состояния, которые были, и адрес, верный на день записи, остаётся
|
||||
верным как свидетельство.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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`** и
|
||||
отдельным файлом не остаётся: два дома для одного замысла разойдутся на
|
||||
|
||||
@@ -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`.
|
||||
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
|
||||
и названия — дело проекта (умолчание `Ядро` / `Инфра`). **В конфиге их нет** —
|
||||
второй список разошёлся бы с заголовками молча.
|
||||
|
||||
@@ -66,7 +66,7 @@ python3 $tk adopt apply --plan tasks-adopt-plan.json \
|
||||
всегда `tasks`. Секции беклога (`--sections`) — по умолчанию
|
||||
`Ядро,Инфра`; если у проекта деление другое по существу, оно называется
|
||||
здесь, а не подгоняется под умолчание, и становится **заголовками `##`
|
||||
индекса** — их единственным домом. В `.tasks.json` секции не пишутся: там
|
||||
индекса** — их единственным домом. В `.av-dev.toml` секции не пишутся: там
|
||||
версия формата и имена частей, а второй список секций разошёлся бы с
|
||||
заголовками молча.
|
||||
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
|
||||
|
||||
@@ -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/ задачи и цели файлами, <slug>.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']}/<slug>.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()
|
||||
|
||||
@@ -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": "рабочие заметки",
|
||||
|
||||
Reference in New Issue
Block a user