конфиг: одна версия и один служебный файл, .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
|
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
|
tools: Read, Grep, Glob, Bash
|
||||||
model: sonnet
|
model: sonnet
|
||||||
color: green
|
color: green
|
||||||
@@ -37,7 +37,7 @@ color: green
|
|||||||
|
|
||||||
## Что тебе дают
|
## Что тебе дают
|
||||||
|
|
||||||
Корень проекта. Читаешь `CLAUDE.md`, `docs/**`, `docs/.docs.json`,
|
Корень проекта. Читаешь `CLAUDE.md`, `docs/**`, `.av-dev.toml`,
|
||||||
`openspec/specs/**` — и репозиторий: манифесты зависимостей, конфиги, файлы
|
`openspec/specs/**` — и репозиторий: манифесты зависимостей, конфиги, файлы
|
||||||
сборки и CI, дерево пакетов.
|
сборки и CI, дерево пакетов.
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ color: green
|
|||||||
держит прежнее имя.
|
держит прежнее имя.
|
||||||
|
|
||||||
3. **Пути** — все, которые канон обязывает называть: `migrations` из
|
3. **Пути** — все, которые канон обязывает называть: `migrations` из
|
||||||
`docs/.docs.json`, `testdata`, временный каталог, пути в запретах `CLAUDE.md`.
|
`.av-dev.toml`, `testdata`, временный каталог, пути в запретах `CLAUDE.md`.
|
||||||
Проверка: существует ли. Путь в запрете, которого нет, — находка **особого
|
Проверка: существует ли. Путь в запрете, которого нет, — находка **особого
|
||||||
рода**: запрет, который не на что наложить, читается как соблюдённый, а на
|
рода**: запрет, который не на что наложить, читается как соблюдённый, а на
|
||||||
деле охраняет пустоту, пока настоящий каталог зовётся иначе.
|
деле охраняет пустоту, пока настоящий каталог зовётся иначе.
|
||||||
@@ -147,7 +147,7 @@ color: green
|
|||||||
```
|
```
|
||||||
факт источник проверено чем итог
|
факт источник проверено чем итог
|
||||||
имя основной ветки CLAUDE.md git branch сошлось
|
имя основной ветки CLAUDE.md git branch сошлось
|
||||||
путь миграций docs/.docs.json ls РАЗОШЛОСЬ
|
путь миграций .av-dev.toml ls РАЗОШЛОСЬ
|
||||||
внешние зависимости architecture.md go.mod 2 не названы
|
внешние зависимости architecture.md go.mod 2 не названы
|
||||||
единые точки: парсер входа architecture.md grep по формату сошлось
|
единые точки: парсер входа architecture.md grep по формату сошлось
|
||||||
настройки БД database.md — не проверено
|
настройки БД database.md — не проверено
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ color: green
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **тема** | `conventions.*`, `security.*`, `architecture.*`, любой свой документ проекта | заводишь строку темы и назначаешь исполнителя |
|
| **тема** | `conventions.*`, `security.*`, `architecture.*`, любой свой документ проекта | заводишь строку темы и назначаешь исполнителя |
|
||||||
| **источник темы** | `passport.*`, `database.*` | называешь адресом **внутри** строки чужой темы, своей строки не заводишь |
|
| **источник темы** | `passport.*`, `database.*` | называешь адресом **внутри** строки чужой темы, своей строки не заводишь |
|
||||||
| **процессный** | `tasks/`, `review.*`, `adr.*`, `research.*`, `.docs.json` | называешь строкой «процессный», исполнителя нет и не должно быть |
|
| **процессный** | `tasks/`, `review.*`, `adr.*`, `research.*`, `.av-dev.toml` | называешь строкой «процессный», исполнителя нет и не должно быть |
|
||||||
|
|
||||||
`docs/review.*` при этом ты читаешь — но как **настройку конвейера**, откуда
|
`docs/review.*` при этом ты читаешь — но как **настройку конвейера**, откуда
|
||||||
берутся вопросы по темам и триггеры метки, а не как тему. `adr.*` и `research.*`
|
берутся вопросы по темам и триггеры метки, а не как тему. `adr.*` и `research.*`
|
||||||
@@ -128,7 +128,7 @@ color: green
|
|||||||
**Каждая запись в `docs/` обязана попасть в план строкой своей категории.** Не «я
|
**Каждая запись в `docs/` обязана попасть в план строкой своей категории.** Не «я
|
||||||
посмотрел и решил» — перечислением. Это и есть проверка твоей работы: план
|
посмотрел и решил» — перечислением. Это и есть проверка твоей работы: план
|
||||||
сверяется с `ls 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.*`, свои документы проекта |
|
| **тема** | заводит направление проверки и требует исполнителя | `conventions.*`, `security.*`, `architecture.*`, свои документы проекта |
|
||||||
| **источник темы** | читает как материал чужой темы, своей не порождает | `passport.*`, `database.*`, `CLAUDE.md`/`AGENTS.md`, `openspec/specs/` |
|
| **источник темы** | читает как материал чужой темы, своей не порождает | `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.*`.** В ней лежит
|
**Одна процессная запись всё же читается — `docs/review.*`.** В ней лежит
|
||||||
настройка самого конвейера: вопросы по темам, журнал дефектов, типовые узлы,
|
настройка самого конвейера: вопросы по темам, журнал дефектов, типовые узлы,
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ description: Привести проект к канону документов
|
|||||||
записок разведки, и дом у них общий — `shared/language.md` в репозитории
|
записок разведки, и дом у них общий — `shared/language.md` в репозитории
|
||||||
плагинов, а этот файл его копия. Вычитывают их два прохода по охвату:
|
плагинов, а этот файл его копия. Вычитывают их два прохода по охвату:
|
||||||
документы — `doc-wording`, записи каталога задач — `task-wording`.
|
документы — `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)**:
|
2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**:
|
||||||
незаполненное — одной честной информативной строкой, а не «TBD»;
|
незаполненное — одной честной информативной строкой, а не «TBD»;
|
||||||
3. **OpenSpec, если его нет или `config.yaml` остался примером** — **вызови
|
3. **OpenSpec, если его нет или `config.yaml` остался примером** — **вызови
|
||||||
@@ -209,10 +211,10 @@ capability), `openspec/config.yaml`.
|
|||||||
|
|
||||||
**Шагов в гейте три, и они независимы.** `docs.py check` не тянет за собой
|
**Шагов в гейте три, и они независимы.** `docs.py check` не тянет за собой
|
||||||
ни задачи, ни конвейер: без своих строк дрейф каталога задач и формы
|
ни задачи, ни конвейер: без своих строк дрейф каталога задач и формы
|
||||||
`openspec/config.yaml` перестаёт ловиться совсем. Ставь соседские шаги по
|
`openspec/config.yaml` перестаёт ловиться совсем. Ставь соседние шаги по
|
||||||
следу присутствия — `<каталог задач>/.tasks.json` есть, значит ставится
|
следу присутствия — каталог задач с индексом на месте, значит ставится
|
||||||
`tasks.py check --dir <каталог задач>`; `openspec/config.yaml` есть, значит
|
`tasks.py check --dir <каталог задач>`; `openspec/config.yaml` есть, значит
|
||||||
ставится `openspec.py check`. Следа нет — плагина в проекте нет, шаг не
|
ставится `openspec.py check`. Следа нет — этой части в проекте нет, шаг не
|
||||||
ставится, и это **строка доклада**, а не поломка: назови, чего теперь не
|
ставится, и это **строка доклада**, а не поломка: назови, чего теперь не
|
||||||
проверяет никто. У каждого шага своя переменная пути с тем же умолчанием на
|
проверяет никто. У каждого шага своя переменная пути с тем же умолчанием на
|
||||||
канонический путь маркетплейса; `$CLAUDE_PLUGIN_ROOT` в гейт не подставляй —
|
канонический путь маркетплейса; `$CLAUDE_PLUGIN_ROOT` в гейт не подставляй —
|
||||||
@@ -273,7 +275,8 @@ capability), `openspec/config.yaml`.
|
|||||||
3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии
|
3. Иначе иди по [changelog.md](references/changelog.md) снизу вверх от версии
|
||||||
проекта до текущей и делай названное в каждой записи. Записи независимы и
|
проекта до текущей и делай названное в каждой записи. Записи независимы и
|
||||||
применяются по порядку.
|
применяются по порядку.
|
||||||
4. Подними `canon` в `docs/.docs.json` до текущей.
|
4. Подними `version` в `.av-dev.toml` до текущей — правь **строку**, а не
|
||||||
|
переписывай файл: комментарии в нём принадлежат проекту.
|
||||||
5. `docs.py check`.
|
5. `docs.py check`.
|
||||||
6. **Позови судей** — Skill `av-dev:doc-healthcheck`.
|
6. **Позови судей** — Skill `av-dev:doc-healthcheck`.
|
||||||
7. **Позови вычитку** — агент `doc-wording`, но **только по тем документам,
|
7. **Позови вычитку** — агент `doc-wording`, но **только по тем документам,
|
||||||
@@ -284,17 +287,16 @@ capability), `openspec/config.yaml`.
|
|||||||
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
|
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
|
||||||
это дефект журнала, и о нём надо сказать, а не догадываться.
|
это дефект журнала, и о нём надо сказать, а не догадываться.
|
||||||
|
|
||||||
**Каталог задач повышается своим журналом, а не этим.** У него своя версия
|
**Каталог задач повышается этим же журналом.** Версия одна на всю раскладку —
|
||||||
формата — ключ `tasks` в `<каталог задач>/.tasks.json`, — и двигает её плагин
|
`version` в `.av-dev.toml`, — и записи журнала говорят про обе половины: и про
|
||||||
`av-dev-tasks`. Запись канона вправе сказать «позови соседа», но не вправе
|
документы, и про каталог задач. Порознь версии жили, пока плагинов было три и
|
||||||
двигать чужое число: две версии, которые ходят по одному журналу, разъезжаются
|
проект мог взять одну половину без другой; с одним плагином два числа означали
|
||||||
на первом же проекте, поставившем один плагин без другого. Отстал каталог
|
бы только вопрос, по какому журналу повышать. Что каталог задач отстал, скажет
|
||||||
задач — это скажет `tasks.py check` своей строкой гейта, а повысит скилл
|
`tasks.py check` своей строкой гейта — той же версией, что и `docs.py`.
|
||||||
`av-dev:task-track`.
|
|
||||||
|
|
||||||
**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.docs.json` с
|
**Шаг 6 обязателен, и вот почему.** `check` сверяет **число** в `.av-dev.toml`
|
||||||
версией скрипта — и только его. Применена ли запись журнала **по существу**, он
|
с версией скрипта — и только его. Применена ли запись журнала **по существу**,
|
||||||
не знает: проект несёт `"canon": 6` и может не иметь того, чего требовала любая
|
он не знает: проект несёт `version = 6` и может не иметь того, чего требовала любая
|
||||||
из пройденных версий. Записи применяются руками (переименовать секцию, проставить
|
из пройденных версий. Записи применяются руками (переименовать секцию, проставить
|
||||||
типы, дописать раздел каждому `fix`), а ручной проход по нескольким записям
|
типы, дописать раздел каждому `fix`), а ручной проход по нескольким записям
|
||||||
подряд — ровно то место, где половина шага делается и забывается. Судьи и есть
|
подряд — ровно то место, где половина шага делается и забывается. Судьи и есть
|
||||||
|
|||||||
@@ -45,8 +45,9 @@
|
|||||||
CLAUDE.md памятка агенту: что это, стек, инварианты с
|
CLAUDE.md памятка агенту: что это, стек, инварианты с
|
||||||
severity, команды, семантика гейта, запреты
|
severity, команды, семантика гейта, запреты
|
||||||
AGENTS.md необязателен, лежит рядом; читается теми же
|
AGENTS.md необязателен, лежит рядом; читается теми же
|
||||||
|
.av-dev.toml версия раскладки и настройки проверок; лежит
|
||||||
|
в корне, потому что нужен и без docs/
|
||||||
docs/
|
docs/
|
||||||
.docs.json версия канона и пути, нужные проверкам
|
|
||||||
passport.md | passport/ зачем и для кого; чем НЕ является; сценарии
|
passport.md | passport/ зачем и для кого; чем НЕ является; сценарии
|
||||||
architecture.md | architecture/ как сложено — обзор; окружение и эксплуатация
|
architecture.md | architecture/ как сложено — обзор; окружение и эксплуатация
|
||||||
database.md | database/ схема хранилища; представление данных и настройки
|
database.md | database/ схема хранилища; представление данных и настройки
|
||||||
@@ -100,7 +101,7 @@ openspec/
|
|||||||
| `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) |
|
| `review.*` | процессный | — (настройка самого конвейера, слой **над** темами) |
|
||||||
| `adr.*` | процессный | — |
|
| `adr.*` | процессный | — |
|
||||||
| `research.*` | процессный | — |
|
| `research.*` | процессный | — |
|
||||||
| `.docs.json` | процессный | — (служебный файл, не документ) |
|
| `.av-dev.toml` | процессный | — (служебный файл, не документ) |
|
||||||
|
|
||||||
**Список тем открытый, и это не послабление, а механизм.** Категории
|
**Список тем открытый, и это не послабление, а механизм.** Категории
|
||||||
`источник` и `процессный` **закрыты** — они перечислены здесь поимённо и
|
`источник` и `процессный` **закрыты** — они перечислены здесь поимённо и
|
||||||
@@ -343,10 +344,10 @@ kebab-case.** Причина не эстетическая: имя файла с
|
|||||||
|
|
||||||
### `tasks/`
|
### `tasks/`
|
||||||
|
|
||||||
**Каталог задач канону не принадлежит.** Его ведёт отдельный плагин
|
**Каталог задач канону не принадлежит.** Его ведёт скилл `task-track` — своим
|
||||||
`av-dev-tasks` — своим скриптом, своим конфигом `<каталог>/.tasks.json`, своей
|
скриптом и своими проверками; где каталог лежит и как названы его части, говорит
|
||||||
версией формата в нём же и своим журналом версий. Канон о том числе не
|
секция `[tasks]` в `.av-dev.toml`. Версия там одна на всю раскладку: канон и
|
||||||
высказывается и его не двигает: повышает каталог задач тот, кто его ведёт.
|
каталог задач двигаются вместе, потому что двигает их один плагин.
|
||||||
Канон **резервирует место** в `docs/` и внутрь не смотрит:
|
Канон **резервирует место** в `docs/` и внутрь не смотрит:
|
||||||
`docs.py` каталог не открывает, его отсутствия не считает дрейфом и согласованность
|
`docs.py` каталог не открывает, его отсутствия не считает дрейфом и согласованность
|
||||||
задач не проверяет. Проект, поставивший только канон документов, задач не ведёт
|
задач не проверяет. Проект, поставивший только канон документов, задач не ведёт
|
||||||
@@ -538,39 +539,41 @@ kebab-case.** Причина не эстетическая: имя файла с
|
|||||||
архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт
|
архитектуру с кодом» задача без дна, и агент, которому её поставили, выдаёт
|
||||||
правдоподобную труху вместо находок.
|
правдоподобную труху вместо находок.
|
||||||
|
|
||||||
## `docs/.docs.json`
|
## `.av-dev.toml`
|
||||||
|
|
||||||
```json
|
```toml
|
||||||
{
|
# Раскладка av-dev в этом проекте: версия и настройки проверок.
|
||||||
"canon": <текущая версия>,
|
|
||||||
"migrations": "internal/store/migrations"
|
version = 1 # версия раскладки
|
||||||
}
|
|
||||||
|
[docs]
|
||||||
|
migrations = "internal/store/migrations" # если БД есть
|
||||||
|
|
||||||
|
[tasks]
|
||||||
|
dir = "tasks" # каталог задач от корня репозитория
|
||||||
```
|
```
|
||||||
|
|
||||||
`canon` — версия канона, под которую проект приведён, целым числом: обратной
|
`version` — версия раскладки, под которую проект приведён, целым числом:
|
||||||
совместимости у канона нет, есть «приведён» и «не приведён». Число подставляет
|
обратной совместимости нет, есть «приведён» и «не приведён». Число подставляет
|
||||||
`init`, `adopt` или `upgrade`, и берётся оно из `docs.py version`, а не из
|
`init`, `adopt` или `upgrade`, и берётся оно из `docs.py version`, а не из
|
||||||
образца: литерал в образце протухает на первом же повышении канона.
|
образца: литерал в образце протухает на первом же повышении.
|
||||||
`migrations` — путь каталога миграций, если БД есть; по нему `docs.py` делает
|
`[docs] migrations` — путь каталога миграций, если БД есть; по нему `docs.py`
|
||||||
сверку с `database.md`.
|
делает сверку с `database.md`. `[tasks]` — где лежит каталог задач и как названы
|
||||||
|
его части; состав ключей описывает скилл `task-track`.
|
||||||
|
|
||||||
**Имя файла — имя плагина, который его завёл.** Канон документов ведёт
|
**Формат TOML взят ради комментариев.** Файл лежит в репозитории проекта, и
|
||||||
`av-dev-docs`, поэтому `.docs.json`; у каталога задач по тому же правилу
|
человек, открывший его через полгода, обязан прочитать в нём, что означает
|
||||||
`.tasks.json`, у конвейера — `openspec/config.yaml`. До версии 13 файл звался
|
число. JSON комментариев не знает, и объяснение приходилось держать в другом
|
||||||
`.pm.json` — по плагину `av-dev-pm`, который распался на четыре и которого
|
файле. Отсюда же правило записи: скрипты правят **строку**, а не переписывают
|
||||||
больше нет; имя пережило владельца и указывало в пустоту. Прежнее имя `docs.py`
|
файл — перезапись стёрла бы то, ради чего формат и выбран.
|
||||||
не читает: два дома для одной версии канона расходятся молча, а переименование
|
|
||||||
стоит одну команду (версия 13 журнала, и `check` называет её сам, когда видит
|
|
||||||
старый файл).
|
|
||||||
|
|
||||||
**Ключа `tasks` здесь больше нет.** Настройки каталога задач вернулись в свой
|
**Файл один, и лежит он в корне.** До слияния плагинов их было два —
|
||||||
файл `<каталог задач>/.tasks.json`, потому что ведёт их другой плагин: конфиг,
|
`docs/.docs.json` с версией канона и `<каталог задач>/.tasks.json` с версией
|
||||||
лежащий в `docs/`, был бы домом, которого нет у проекта, поставившего учёт работ
|
формата задач, — и версии двигались порознь, потому что плагины ставились
|
||||||
без канона документов. Состав ключей описывает тот плагин, а не канон. Там же —
|
порознь. Плагин теперь один, версия одна, а корень выбран потому, что он есть и
|
||||||
**версия формата задач**, и она своя: у проекта без `docs/` версии канона нет
|
у проекта без `docs/`, и у проекта без каталога задач. Прежние имена не
|
||||||
вовсе, сверять её было бы не с чем. Прежний ключ читается, пока живы
|
читаются: два дома для одной версии расходятся молча. Увидев их, `docs.py` и
|
||||||
непереехавшие проекты, и `tasks.py` говорит о нём замечанием на каждом
|
`tasks.py` говорят «прежняя раскладка» и зовут `upgrade` — версия 1 журнала.
|
||||||
прогоне — версия 8 журнала просит его убрать.
|
|
||||||
|
|
||||||
Ключей будет больше по мере роста проверок; неизвестный ключ `docs.py`
|
Ключей будет больше по мере роста проверок; неизвестный ключ `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
|
Одна запись на версию. Проект знает свою версию из ключа `version` в
|
||||||
upgrade` идёт по записям снизу вверх от версии проекта до текущей и делает то,
|
`.av-dev.toml`; `canon upgrade` идёт по записям снизу вверх от версии проекта до
|
||||||
что в них названо. Записи ниже версии 13 зовут этот файл прежним именем,
|
текущей и делает то, что в них названо.
|
||||||
`docs/.pm.json`, — так и было на день записи, и переписывать историю мы не
|
|
||||||
станем; переименование делает запись 13.
|
|
||||||
|
|
||||||
**Каталог задач этим журналом не повышается.** У него своя версия формата и свой
|
|
||||||
журнал — `references/changelog.md` скилла `av-dev-tasks:tasks`. Записи 8, 11 и 12
|
|
||||||
трогали его в те времена, когда своего числа у него не было; впредь запись канона
|
|
||||||
вправе позвать соседа, но не двигать его версию.
|
|
||||||
|
|
||||||
Правило записи: **что добавилось, что переехало, что удалено, что сделать
|
Правило записи: **что добавилось, что переехало, что удалено, что сделать
|
||||||
проекту**. Без последнего пункта запись бесполезна — по ней и работает
|
проекту**. Без последнего пункта запись бесполезна — по ней и работает
|
||||||
`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 стало два законных источника. Прежде запись цитировала только архивный
|
Три плагина — `av-dev-docs`, `av-dev-tasks` и `av-dev-code` — слились в один,
|
||||||
`design.md`, то есть решение, принятое по ходу изменения. Решение, принятое
|
`av-dev`. Раскол делался под раздельную установку: проект мог взять учёт работ
|
||||||
**разведкой** — намеренный отказ, выбор подхода, «проверили и не делаем», — не
|
без документов канона или конвейер без обоих. Практикой посылка не подтвердилась
|
||||||
имеет `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` |
|
| `docs/.docs.json`, ключ `canon` | `.av-dev.toml` в корне, ключ `version` |
|
||||||
| правило заведения ADR в `docs/adr/README.md` | [canon.md](canon.md), раздел `adr/` |
|
| `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`
|
## `docs/security.md`
|
||||||
|
|
||||||
@@ -440,24 +440,32 @@ severity стоит здесь, а не выводится каждым прох
|
|||||||
говорит (единственный дом, а не форма) — [canon.md](canon.md), раздел
|
говорит (единственный дом, а не форма) — [canon.md](canon.md), раздел
|
||||||
`openspec/config.yaml`.
|
`openspec/config.yaml`.
|
||||||
|
|
||||||
## `docs/.docs.json`
|
## `.av-dev.toml`
|
||||||
|
|
||||||
```json
|
```toml
|
||||||
{
|
# Раскладка av-dev в этом проекте: версия и настройки проверок.
|
||||||
"canon": <текущая версия>
|
|
||||||
}
|
version = <текущая версия>
|
||||||
|
|
||||||
|
[docs]
|
||||||
|
# migrations = "<путь>" — появится, когда появится БД
|
||||||
|
|
||||||
|
[tasks]
|
||||||
|
dir = "tasks"
|
||||||
```
|
```
|
||||||
|
|
||||||
`<текущая версия>` подставляет `init` или `adopt`, целым числом; берётся она из
|
`<текущая версия>` подставляет `init` или `adopt`, целым числом; берётся она из
|
||||||
`docs.py version` (строка «канон скрипта»), а не из памяти. Литерал здесь
|
`docs.py version` (строка «раскладка скрипта»), а не из памяти. Литерал здесь
|
||||||
протухает при каждом повышении канона, поэтому его тут и нет: незамещённый
|
протухает при каждом повышении, поэтому его тут и нет: незамещённый плейсхолдер
|
||||||
плейсхолдер ломает разбор JSON громко, а отставшее число дало бы дрейф молча.
|
ломает разбор TOML громко, а отставшее число дало бы дрейф молча.
|
||||||
|
|
||||||
Плюс `"migrations": "<путь>"`, если есть БД. Ключа `"tasks"` здесь **нет**:
|
Комментарии в файле — не украшение, а причина, по которой взят TOML: файл живёт
|
||||||
настройки каталога задач и версия их формата переехали в свой файл `<каталог
|
в чужом репозитории, и назначение числа читают из него самого. Скрипты это
|
||||||
задач>/.tasks.json`, потому что ведёт их другой плагин. Состав ключей —
|
учитывают и правят строку, а не переписывают файл. Состав ключей —
|
||||||
[canon.md](canon.md).
|
[canon.md](canon.md), раздел `.av-dev.toml`.
|
||||||
|
|
||||||
Имя файла — по плагину-владельцу, `av-dev-docs`. До версии 13 он звался
|
Файл лежит **в корне репозитория**, а не в `docs/`: версия одна на всю
|
||||||
`.pm.json`, по распавшемуся `av-dev-pm`; проект с прежним именем `docs.py check`
|
раскладку, и нужна она в том числе проекту, который канон документов ещё не
|
||||||
называет отдельной строкой и зовёт переименовать.
|
завёл. Прежние `docs/.docs.json` и `<каталог задач>/.tasks.json` остались от
|
||||||
|
трёх плагинов, слившихся в один; увидев их, `docs.py check` называет это прежней
|
||||||
|
раскладкой и зовёт `upgrade`.
|
||||||
|
|||||||
@@ -17,26 +17,47 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import importlib.util
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
|
|
||||||
CANON_VERSION = 14
|
|
||||||
|
|
||||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||||
|
|
||||||
# Дом версии канона и путей, нужных проверкам. Имя — от плагина, который файл
|
|
||||||
# завёл: настройки канона документов ведёт `av-dev-docs`, и файл называется по
|
def _load_shared() -> ModuleType:
|
||||||
# нему. Прежнее имя досталось от `av-dev-pm` — плагина, который распался на
|
"""Общий читатель `.av-dev.toml` — `shared/config.py` этого же плагина.
|
||||||
# четыре и которого больше нет; читать его скрипт не умеет намеренно, потому что
|
|
||||||
# два дома для версии канона расходятся молча, а переименование стоит одну
|
Путь считается от файла скрипта, а не от рабочего каталога: скрипт зовут из
|
||||||
# команду и названо записью 13 журнала.
|
репозитория проекта, где ни плагина, ни его дерева в текущем каталоге нет.
|
||||||
CONFIG = "docs/.docs.json"
|
Своё дерево — единственное, куда ходить можно; в чужое не ходим никогда.
|
||||||
LEGACY_CONFIG = "docs/.pm.json"
|
"""
|
||||||
|
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/.
|
# Обязательные файлы вне раскладки docs/.
|
||||||
REQUIRED = {
|
REQUIRED = {
|
||||||
"CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта",
|
"CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта",
|
||||||
CONFIG: "версия канона и пути, нужные проверкам",
|
CONFIG: "версия раскладки av-dev и пути, нужные проверкам",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Файлы, которые документ-каталог обязан держать сверх README.md.
|
# Файлы, которые документ-каталог обязан держать сверх README.md.
|
||||||
@@ -86,9 +107,10 @@ DOC_EXTRA = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Служебное в docs/ и каталог задач, оставшийся там от прежней раскладки. Формы
|
# Служебное в docs/ и каталог задач, оставшийся там от прежней раскладки. Формы
|
||||||
# у них скрипт не проверяет, и по разным причинам: `.docs.json` не markdown, а
|
# у них скрипт не проверяет, и по разным причинам: `.docs.json` не markdown (и
|
||||||
# задачи **принадлежат другому плагину** — `av-dev-tasks`, со своим скриптом,
|
# сам он теперь след прежней раскладки, о котором говорит `check_required`), а
|
||||||
# своим конфигом и своей версией формата (её сторожит `tasks.py check`).
|
# задачи ведёт **другой скилл** — `task-track`, со своим скриптом и своими
|
||||||
|
# проверками.
|
||||||
#
|
#
|
||||||
# Дом задач с версии 11 — `tasks/` в корне репозитория, то есть вне `docs/`
|
# Дом задач с версии 11 — `tasks/` в корне репозитория, то есть вне `docs/`
|
||||||
# вовсе. `docs/tasks/` здесь терпится потому, что непереехавший проект не должен
|
# вовсе. `docs/tasks/` здесь терпится потому, что непереехавший проект не должен
|
||||||
@@ -106,11 +128,11 @@ NOT_DOCS = {".docs.json", ".pm.json", "tasks"}
|
|||||||
RETIRED = {
|
RETIRED = {
|
||||||
"review-brief.md": "документы канона и есть бриф; остаток — в review",
|
"review-brief.md": "документы канона и есть бриф; остаток — в review",
|
||||||
"review-journal.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",
|
"local-research.md": "→ документ research",
|
||||||
"specs": "поведение → openspec/specs/, обзор → тема architecture",
|
"specs": "поведение → openspec/specs/, обзор → тема architecture",
|
||||||
"drafts": "идея → запись research, отказ → ADR, порядок → ROADMAP.md",
|
"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:
|
def read_config(root: Path, rep: Report) -> dict:
|
||||||
path = root / CONFIG
|
"""Настройки проекта целиком; проверкам канона нужна секция `[docs]`."""
|
||||||
if not path.exists():
|
|
||||||
return {}
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
return conf.read(root)
|
||||||
except json.JSONDecodeError as exc:
|
except conf.ConfigError as exc:
|
||||||
fail(ENV, f"{CONFIG} не разбирается: {exc}")
|
fail(ENV, str(exc))
|
||||||
if not isinstance(data, dict):
|
|
||||||
fail(ENV, f"{CONFIG} должен быть объектом")
|
|
||||||
return data
|
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:
|
def check_version(root: Path, cfg: dict, rep: Report) -> None:
|
||||||
if not (root / CONFIG).exists():
|
if not (root / CONFIG).exists():
|
||||||
return # об отсутствии файла скажет check_required, второй раз не нужно
|
return # об отсутствии файла скажет check_required, второй раз не нужно
|
||||||
if "canon" not in cfg:
|
got = conf.version(cfg)
|
||||||
rep.error(f"в {CONFIG} нет ключа canon — версия канона не объявлена")
|
if got is None:
|
||||||
return
|
rep.error(f"в {CONFIG} нет ключа version — версия раскладки не объявлена")
|
||||||
got = cfg["canon"]
|
|
||||||
if not isinstance(got, int):
|
|
||||||
rep.error(f"canon в {CONFIG} должен быть целым числом, а не {got!r}")
|
|
||||||
return
|
return
|
||||||
if got < CANON_VERSION:
|
if got < CANON_VERSION:
|
||||||
rep.error(
|
rep.error(
|
||||||
f"проект приведён к канону версии {got}, текущая — {CANON_VERSION}: "
|
f"проект приведён к раскладке версии {got}, текущая — {CANON_VERSION}: "
|
||||||
f"нужен canon upgrade"
|
f"нужен canon upgrade"
|
||||||
)
|
)
|
||||||
elif got > CANON_VERSION:
|
elif got > CANON_VERSION:
|
||||||
rep.error(
|
rep.error(
|
||||||
f"проект приведён к канону версии {got}, а скрипт знает {CANON_VERSION}: "
|
f"проект приведён к раскладке версии {got}, а скрипт знает"
|
||||||
f"устарел плагин, обнови маркетплейс"
|
f" {CANON_VERSION}: устарел плагин, обнови маркетплейс"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -331,15 +349,17 @@ def check_required(root: Path, cfg: dict, rep: Report) -> None:
|
|||||||
for rel, what in REQUIRED.items():
|
for rel, what in REQUIRED.items():
|
||||||
if (root / rel).exists():
|
if (root / rel).exists():
|
||||||
continue
|
continue
|
||||||
# Файл под прежним именем — это не «нет файла», а незаконченный переезд,
|
# Настройки под прежними именами — это не «нет файла», а незаконченный
|
||||||
# и чинится он одной командой. Без этой ветки проект услышал бы «нет
|
# переезд. Без этой ветки проект слышал бы «нет версии» и шёл заводить
|
||||||
# версии канона» и пошёл заводить второй файл рядом с первым.
|
# второй файл рядом с первым, а старые остались бы вторым домом.
|
||||||
if rel == CONFIG and (root / LEGACY_CONFIG).exists():
|
legacy = conf.legacy_files(root)
|
||||||
|
if rel == CONFIG and legacy:
|
||||||
rep.error(
|
rep.error(
|
||||||
f"нет {rel} — {what}. Настройки лежат под прежним именем"
|
f"нет {rel} — {what}. Настройки лежат по прежней раскладке"
|
||||||
f" {LEGACY_CONFIG} (от плагина av-dev-pm, которого больше нет):"
|
f" ({', '.join(legacy)}) — она осталась от трёх плагинов, которые"
|
||||||
f" `git mv {LEGACY_CONFIG} {rel}` — журнал канона, версия 13."
|
f" слились в один: перенеси значения и удали старые файлы"
|
||||||
f" Прежнее имя не читается, поэтому в этом прогоне всё"
|
f" операцией upgrade скилла av-dev:doc-canon (журнал, версия 1)."
|
||||||
|
f" Прежние имена не читаются, поэтому в этом прогоне всё"
|
||||||
f" остальное проверено так, будто настроек нет вовсе"
|
f" остальное проверено так, будто настроек нет вовсе"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -360,18 +380,20 @@ def check_required(root: Path, cfg: dict, rep: Report) -> None:
|
|||||||
if not (home / extra).is_file():
|
if not (home / extra).is_file():
|
||||||
rep.error(f"нет docs/{name}/{extra} — {why}")
|
rep.error(f"нет docs/{name}/{extra} — {why}")
|
||||||
|
|
||||||
|
docs = docs_cfg(cfg)
|
||||||
for name, (key, kind, what) in CONDITIONAL_DOCS.items():
|
for name, (key, kind, what) in CONDITIONAL_DOCS.items():
|
||||||
home, complaint = doc_home(root, name)
|
home, complaint = doc_home(root, name)
|
||||||
if complaint:
|
if complaint:
|
||||||
rep.error(complaint)
|
rep.error(complaint)
|
||||||
if key in cfg and home is None:
|
if key in docs and home is None:
|
||||||
rep.error(
|
rep.error(
|
||||||
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
|
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
|
||||||
f" категория «{kind}» — {what}"
|
f" категория «{kind}» — {what}"
|
||||||
f" (обязателен: в .docs.json объявлен {key})"
|
f" (обязателен: в {CONFIG} объявлен [docs] {key})"
|
||||||
)
|
)
|
||||||
elif key not in cfg and home is None:
|
elif key not in docs and home is None:
|
||||||
rep.skip(f"{name} — в .docs.json нет ключа {key}, проверка неприменима")
|
rep.skip(f"{name} — в {CONFIG} нет ключа [docs] {key},"
|
||||||
|
f" проверка неприменима")
|
||||||
|
|
||||||
|
|
||||||
def check_stray(root: Path, rep: Report) -> None:
|
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:
|
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:
|
if not migrations:
|
||||||
rep.skip("в .docs.json нет ключа migrations — сверка со схемой неприменима")
|
rep.skip(f"в {CONFIG} нет ключа [docs] migrations —"
|
||||||
|
f" сверка со схемой неприменима")
|
||||||
return
|
return
|
||||||
if not base:
|
if not base:
|
||||||
rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась")
|
rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась")
|
||||||
@@ -630,9 +653,9 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|||||||
def cmd_version(args: argparse.Namespace) -> int:
|
def cmd_version(args: argparse.Namespace) -> int:
|
||||||
root = Path(args.dir).resolve()
|
root = Path(args.dir).resolve()
|
||||||
cfg = read_config(root, Report())
|
cfg = read_config(root, Report())
|
||||||
got = cfg.get("canon", "не объявлена")
|
got = conf.version(cfg)
|
||||||
print(f"канон скрипта: {CANON_VERSION}")
|
print(f"раскладка скрипта: {CANON_VERSION}")
|
||||||
print(f"канон проекта: {got}")
|
print(f"раскладка проекта: {got if got is not None else 'не объявлена'}")
|
||||||
return OK
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ description: "Завести новый проект — сессия вопро
|
|||||||
| `passport.md` | `architecture.md` |
|
| `passport.md` | `architecture.md` |
|
||||||
| `CLAUDE.md` | `database.md` |
|
| `CLAUDE.md` | `database.md` |
|
||||||
| `security.md` | `conventions/` |
|
| `security.md` | `conventions/` |
|
||||||
| `docs/.docs.json` | `research/`, `adr/` |
|
| `.av-dev.toml` | `research/`, `adr/` |
|
||||||
| | `review.md` — журнал пуст, настройка появится с первым ревью |
|
| | `review.md` — журнал пуст, настройка появится с первым ревью |
|
||||||
|
|
||||||
Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет,
|
Честная строка информативна, а не «TBD»: «архитектуры пока нет: кода нет,
|
||||||
@@ -125,7 +125,7 @@ description: "Завести новый проект — сессия вопро
|
|||||||
|
|
||||||
**Вызов не разрешился** — проект без конвейера живёт без OpenSpec законно:
|
**Вызов не разрешился** — проект без конвейера живёт без OpenSpec законно:
|
||||||
строка доклада, и дальше; `docs.py check` о каталоге тоже промолчит.
|
строка доклада, и дальше; `docs.py check` о каталоге тоже промолчит.
|
||||||
4. Заведи `docs/.docs.json` с текущей версией канона — число берётся из
|
4. Заведи `.av-dev.toml` в корне с текущей версией раскладки — число берётся из
|
||||||
`docs.py version`, а не из памяти.
|
`docs.py version`, а не из памяти.
|
||||||
5. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и
|
5. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и
|
||||||
отдельным файлом не остаётся: два дома для одного замысла разойдутся на
|
отдельным файлом не остаётся: два дома для одного замысла разойдутся на
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
| 0 | сошлось / сделано | дальше по сценарию |
|
| 0 | сошлось / сделано | дальше по сценарию |
|
||||||
| 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать |
|
| 1 | **только `check`:** найден дрейф индексов и файлов | `check --fix`, остаток разобрать |
|
||||||
| 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу |
|
| 2 | ошибка употребления: аргументы или нарушенное правило | читать сообщение, это отказ по существу |
|
||||||
| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `<каталог задач>/.tasks.json`, повтор не поможет |
|
| 3 | окружение: каталог не найден, конфиг битый или мимо диска | чинится путём или `.av-dev.toml` в корне, повтор не поможет |
|
||||||
| 4 | внутренний сбой | дефект скрипта, доложить |
|
| 4 | внутренний сбой | дефект скрипта, доложить |
|
||||||
|
|
||||||
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
|
Различать 1 и 3 обязательно: «дрейф в беклоге» — рабочая ситуация, «каталога
|
||||||
@@ -494,38 +494,24 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
[fix](references/task-fix.md) · [chore](references/task-chore.md) ·
|
[fix](references/task-fix.md) · [chore](references/task-chore.md) ·
|
||||||
[research](references/task-research.md).
|
[research](references/task-research.md).
|
||||||
|
|
||||||
## Версия формата
|
## Версия раскладки
|
||||||
|
|
||||||
Формат каталога задач меняется, и проект должен знать, к какой его версии
|
Формат каталога задач меняется, и проект должен знать, к какой версии он
|
||||||
приведён. Число живёт ключом `tasks` в `<каталог задач>/.tasks.json`, журнал
|
приведён. Число живёт ключом `version` в `.av-dev.toml` в корне репозитория,
|
||||||
версий — [references/changelog.md](references/changelog.md), сверяет их
|
журнал версий — [журнал скилла `doc-canon`](../doc-canon/references/changelog.md),
|
||||||
`tasks.py check`: отстало — строка расхождения, ушло вперёд — устарел плагин.
|
сверяет их `tasks.py check`: отстало — строка расхождения, ушло вперёд — устарел
|
||||||
|
плагин. Обратной совместимости нет: есть «приведён» и «не приведён».
|
||||||
|
|
||||||
**Версия своя, а не канона документов.** Плагин ставится в одиночку: проект,
|
**Версия одна на всю раскладку — и на документы, и на задачи.** Своя у каталога
|
||||||
взявший учёт работ без `av-dev-docs`, каталога `docs/` не имеет вовсе, а значит
|
задач была, пока плагинов было три и ставились они порознь: проект мог взять
|
||||||
не имеет и версии канона — сверять было бы не с чем. Обратной совместимости у
|
учёт работ без канона документов, и общее число оказалось бы домом, которого у
|
||||||
формата нет: есть «приведён» и «не приведён».
|
половины проектов нет. Плагин один — довод ушёл, а два числа оставляли бы
|
||||||
|
вопрос, по какому журналу повышать.
|
||||||
|
|
||||||
**`upgrade` — повысить каталог до текущего формата:**
|
**Повышает проект скилл `av-dev:doc-canon`, операция `upgrade`** — он идёт по
|
||||||
|
журналу, двигает число и зовёт этот скилл там, где запись касается задач. Здесь
|
||||||
1. `python3 $tk check --dir D` — первая же строка расхождений называет версию
|
повышения нет намеренно: две операции, двигающие одно число, разъезжаются на
|
||||||
проекта и версию скрипта. Проект новее скрипта — **обнови маркетплейс**, а не
|
первом же проекте, где прошла только одна из них.
|
||||||
проект: это отстал плагин.
|
|
||||||
2. Иди по [журналу](references/changelog.md) снизу вверх от версии проекта до
|
|
||||||
текущей и делай названное в каждой записи. Записи независимы и применяются по
|
|
||||||
порядку.
|
|
||||||
3. Подними `tasks` в `.tasks.json` до текущей — руками, последним шагом. Раньше
|
|
||||||
времени поднятое число объявляет каталог приведённым к формату, шагов
|
|
||||||
которого никто не делал; `check --fix` этого не пишет намеренно.
|
|
||||||
4. `check --dir D` ещё раз — до отсутствия расхождений.
|
|
||||||
|
|
||||||
Записи журнала описывают **что сделать проекту**. Если запись этого не говорит —
|
|
||||||
это дефект журнала, и о нём надо сказать, а не догадываться.
|
|
||||||
|
|
||||||
**Канон документов сюда не вмешивается.** Его журнал двигает своё число в
|
|
||||||
`docs/.docs.json` и вправе сказать «позови этот скилл», но не двигать версию
|
|
||||||
формата задач: две версии, ходящие по одному журналу, разъедутся на первом же
|
|
||||||
проекте, где стоит один плагин без другого.
|
|
||||||
|
|
||||||
## Сценарии
|
## Сценарии
|
||||||
|
|
||||||
@@ -689,19 +675,17 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
|||||||
действительно новый, а перевод чужой раскладки делает `av-dev:doc-canon`.
|
действительно новый, а перевод чужой раскладки делает `av-dev:doc-canon`.
|
||||||
У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и
|
У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и
|
||||||
полагаться на него скилл не должен: молча найденный чужой каталог это дрейф.
|
полагаться на него скилл не должен: молча найденный чужой каталог это дрейф.
|
||||||
- **Версия формата и настройки живут в `<каталог задач>/.tasks.json`** — свой
|
- **Версия и настройки живут в `.av-dev.toml` в корне репозитория** — версия
|
||||||
файл у своего плагина: ключ `tasks` с версией формата плюс **имена** файлов и
|
ключом `version`, настройки каталога секцией `[tasks]`: `dir` — где каталог
|
||||||
заголовков, и последние — только если отличаются от умолчания. Неизвестный
|
лежит, плюс **имена** файлов и заголовков, и последние только если отличаются
|
||||||
ключ — код 3 на любой команде, так что лишнее слово в этом объекте
|
от умолчания. Неизвестный ключ в секции — код 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`. Секции беклога (`--sections`) — по умолчанию
|
||||||
`Ядро,Инфра`; если у проекта деление другое по существу, оно называется
|
`Ядро,Инфра`; если у проекта деление другое по существу, оно называется
|
||||||
здесь, а не подгоняется под умолчание, и становится **заголовками `##`
|
здесь, а не подгоняется под умолчание, и становится **заголовками `##`
|
||||||
индекса** — их единственным домом. В `.tasks.json` секции не пишутся: там
|
индекса** — их единственным домом. В `.av-dev.toml` секции не пишутся: там
|
||||||
версия формата и имена частей, а второй список секций разошёлся бы с
|
версия формата и имена частей, а второй список секций разошёлся бы с
|
||||||
заголовками молча.
|
заголовками молча.
|
||||||
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
|
2. **`adopt scan`** по всем источникам разом. Один прогон, одна карта: два
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
ровно в одном из них за раз. `REJECTED.md` индексом не считается: он не говорит,
|
ровно в одном из них за раз. `REJECTED.md` индексом не считается: он не говорит,
|
||||||
где запись числится, он кладбище ушедшего.
|
где запись числится, он кладбище ушедшего.
|
||||||
|
|
||||||
Раскладка. Путь каталога — `tasks/` в корне репозитория, жёстко. Каталог
|
Раскладка. Путь каталога — `tasks/` в корне репозитория по умолчанию; другой
|
||||||
принадлежит этому плагину, а не канону документов: `docs/` ведёт другой плагин, и
|
называется ключом `[tasks] dir`. Каталог принадлежит этому скиллу, а не канону
|
||||||
проект, поставивший учёт работ без него, каталога `docs/` не имеет вовсе. Имена
|
документов: учёт работ ведут и в проекте, который к канону не приведён. Имена
|
||||||
внутри и **версия формата** живут в `tasks/.tasks.json`; журнал версий —
|
частей и **версия раскладки** живут в `.av-dev.toml` в корне; журнал версий —
|
||||||
references/changelog.md рядом со скриптом.
|
references/changelog.md скилла doc-canon.
|
||||||
|
|
||||||
tasks/
|
tasks/
|
||||||
items/ задачи и цели файлами, <slug>.md
|
items/ задачи и цели файлами, <slug>.md
|
||||||
@@ -102,32 +102,47 @@ goal | feature | fix | chore | research, по-английски, как и пр
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime
|
import datetime
|
||||||
|
import importlib.util
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
CONFIG_NAME = ".tasks.json" # дом настроек и версии: свой файл в каталоге
|
|
||||||
PM_CONFIG_REL = "../.pm.json" # прежний дом настроек: docs/.pm.json, ключ "tasks"
|
|
||||||
|
|
||||||
# Версия формата задач — **своя, а не канона документов**. Число живёт ключом
|
def _load_shared() -> ModuleType:
|
||||||
# `tasks` в `.tasks.json`, журнал версий — references/changelog.md рядом со
|
"""Общий читатель `.av-dev.toml` — `shared/config.py` этого же плагина.
|
||||||
# скриптом, повышает его операция `upgrade` скилла `av-dev:task-track`.
|
|
||||||
|
Путь считается от файла скрипта: зовут его из репозитория проекта, где
|
||||||
|
дерева плагина в текущем каталоге нет.
|
||||||
|
"""
|
||||||
|
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/` не имеет вовсе, а значит не имеет
|
# числом в журнал не переписаны: они названы прежними журналами, и второй
|
||||||
# и версии канона — сверять было бы не с чем. Копия чужого числа в этом скрипте
|
# перечень тех же шагов разошёлся бы с первым.
|
||||||
# была бы вторым домом для одной версии и разъехалась бы молча при обновлении
|
FORMAT_VERSION = conf.VERSION
|
||||||
# одного плагина без другого.
|
VERSION_KEY = conf.VERSION_KEY
|
||||||
#
|
|
||||||
# Переезды каталога задач, случившиеся до появления этого числа (в корень —
|
|
||||||
# канон 11, отмена спринтов — канон 12), задним числом сюда не переписаны: они
|
|
||||||
# уже названы журналом канона, и второй перечень тех же шагов разошёлся бы с
|
|
||||||
# первым. Версия 1 — формат на день её появления, что бы проекту ни пришлось
|
|
||||||
# пройти до неё.
|
|
||||||
FORMAT_VERSION = 1
|
|
||||||
VERSION_KEY = "tasks"
|
|
||||||
|
|
||||||
EXIT_OK = 0
|
EXIT_OK = 0
|
||||||
EXIT_DRIFT = 1
|
EXIT_DRIFT = 1
|
||||||
@@ -135,6 +150,11 @@ EXIT_USAGE = 2
|
|||||||
EXIT_ENV = 3
|
EXIT_ENV = 3
|
||||||
EXIT_INTERNAL = 4
|
EXIT_INTERNAL = 4
|
||||||
|
|
||||||
|
# Ключ `dir` в DEFAULTS не входит намеренно: он говорит, **где** каталог, а не
|
||||||
|
# как названы его части, и в `Layout` (тот про имена внутри) ему делать нечего.
|
||||||
|
DIR_KEY = "dir"
|
||||||
|
DEFAULT_DIR = "tasks"
|
||||||
|
|
||||||
DEFAULTS = {
|
DEFAULTS = {
|
||||||
"items": "items",
|
"items": "items",
|
||||||
"backlog": "BACKLOG.md",
|
"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.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"]
|
self.items = root / self.cfg["items"]
|
||||||
|
|
||||||
def index(self, kind: str) -> Path:
|
def index(self, kind: str) -> Path:
|
||||||
@@ -451,64 +476,28 @@ class Layout:
|
|||||||
return ("backlog", "roadmap")
|
return ("backlog", "roadmap")
|
||||||
|
|
||||||
|
|
||||||
def load_config(root: Path) -> dict:
|
def load_config(project: Path) -> dict:
|
||||||
"""Настройки каталога задач и версия его формата.
|
"""Весь `.av-dev.toml` проекта. Секция задач берётся из него отдельно.
|
||||||
|
|
||||||
Дом — `<каталог задач>/.tasks.json`: **свой файл у своего плагина**. Ключ
|
Дом настроек — **корень репозитория**, а не каталог задач: файл держит
|
||||||
`tasks` в `docs/.pm.json` читается, пока живы проекты, заведённые до раскола
|
версию раскладки, которая одна на плагин, и ключ `[tasks] dir`, который
|
||||||
плагинов, и только когда своего файла нет; когда есть оба, побеждает свой, и
|
говорит, где каталог лежит. Настройка внутри настраиваемого каталога не
|
||||||
об этом говорится вслух — молча выбранный из двух конфиг это дрейф, который
|
смогла бы сказать, где он.
|
||||||
потом никто не объяснит.
|
|
||||||
|
|
||||||
Порядок именно такой, а не наоборот, потому что `docs/` принадлежит другому
|
|
||||||
плагину. Проект, поставивший учёт задач без канона документов, каталога
|
|
||||||
`docs/` не имеет вовсе, и дом настроек, лежащий в чужом дереве, был бы домом,
|
|
||||||
которого у половины проектов нет.
|
|
||||||
|
|
||||||
Версия формата (ключ `tasks`) читается **только из своего файла**: прежний
|
|
||||||
дом её не знал и знать не может, и молча выведенная из его отсутствия версия
|
|
||||||
была бы догадкой о том, что чинится одной строкой.
|
|
||||||
"""
|
"""
|
||||||
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:
|
try:
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = conf.read(project)
|
||||||
except json.JSONDecodeError as e:
|
except conf.ConfigError as e:
|
||||||
raise Env(f"{path}: не разбирается как JSON — {e}") from e
|
raise Env(str(e)) from e
|
||||||
if not isinstance(data, dict):
|
_validate_config(conf.section(data, "tasks"), project / CONFIG_NAME)
|
||||||
raise Env(f"{path}: ожидался объект с настройками")
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def tasks_section(full: dict) -> dict:
|
||||||
|
return conf.section(full, "tasks")
|
||||||
|
|
||||||
|
|
||||||
def _validate_config(data: dict, path: Path) -> dict:
|
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.
|
# Ключ «plan» был домом оглавления целей до того, как файл стал ROADMAP.md.
|
||||||
# Без этой ветки проект со старым конфигом получал бы «неизвестный ключ» и
|
# Без этой ветки проект со старым конфигом получал бы «неизвестный ключ» и
|
||||||
# искал опечатку там, где на самом деле переименование канона.
|
# искал опечатку там, где на самом деле переименование канона.
|
||||||
@@ -518,19 +507,12 @@ def _validate_config(data: dict, path: Path) -> dict:
|
|||||||
f" av-dev:doc-canon (upgrade), а не правь ключ в одиночку:"
|
f" av-dev:doc-canon (upgrade), а не правь ключ в одиночку:"
|
||||||
f" файл и ссылки на него переезжают вместе с ним")
|
f" файл и ссылки на него переезжают вместе с ним")
|
||||||
if unknown:
|
if unknown:
|
||||||
known = sorted({*DEFAULTS, VERSION_KEY})
|
known = sorted({*DEFAULTS, DIR_KEY})
|
||||||
raise Env(f"{path}: неизвестные ключи: {', '.join(sorted(unknown))}"
|
raise Env(f"{path}: неизвестные ключи в секции [tasks]:"
|
||||||
f" (известны: {', '.join(known)})")
|
f" {', '.join(sorted(unknown))} (известны: {', '.join(known)})")
|
||||||
# Версия — единственный ключ-число: остальные это имена файлов и заголовков.
|
# Версия раскладки лежит ключом верхнего уровня и проверяется общим
|
||||||
# Битое число тут останавливает работу целиком (код 3), а не идёт дрейфом,
|
# читателем: здесь судится только секция задач, и все её ключи — строки.
|
||||||
# потому что «на какой версии формата каталог» решает, чему верить дальше.
|
|
||||||
got = data.get(VERSION_KEY)
|
|
||||||
if VERSION_KEY in data and (isinstance(got, bool) or not isinstance(got, int)):
|
|
||||||
raise Env(f"{path}: ключ «{VERSION_KEY}» — версия формата задач,"
|
|
||||||
f" ожидалось целое число, а не {got!r}")
|
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if key == VERSION_KEY:
|
|
||||||
continue
|
|
||||||
if not isinstance(value, str) or not value.strip():
|
if not isinstance(value, str) or not value.strip():
|
||||||
raise Env(f"{path}: ключ «{key}» — ожидалась непустая строка")
|
raise Env(f"{path}: ключ «{key}» — ожидалась непустая строка")
|
||||||
if key in PATH_KEYS and (value.startswith("/") or ".." in Path(value).parts):
|
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
|
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
|
path = lay.project / CONFIG_NAME
|
||||||
if path.is_file():
|
return path if path.is_file() else None
|
||||||
return path
|
|
||||||
pm = (root / PM_CONFIG_REL).resolve()
|
|
||||||
return pm if pm.is_file() else None
|
|
||||||
|
|
||||||
|
|
||||||
def config_problems(lay: Layout) -> list[str]:
|
def config_problems(lay: Layout) -> list[str]:
|
||||||
@@ -558,7 +538,7 @@ def config_problems(lay: Layout) -> list[str]:
|
|||||||
check обвинять невиновных: «ссылка на несуществующий файл», хотя файл на
|
check обвинять невиновных: «ссылка на несуществующий файл», хотя файл на
|
||||||
месте, а мимо смотрит конфиг.
|
месте, а мимо смотрит конфиг.
|
||||||
"""
|
"""
|
||||||
where = str(config_home(lay.root) or "умолчания (конфига нет)")
|
where = str(config_home(lay) or "умолчания (конфига нет)")
|
||||||
out = []
|
out = []
|
||||||
if not lay.items.is_dir():
|
if not lay.items.is_dir():
|
||||||
out.append(f"{where}: items = «{lay.cfg['items']}» → {lay.items} — каталога нет")
|
out.append(f"{where}: items = «{lay.cfg['items']}» → {lay.items} — каталога нет")
|
||||||
@@ -582,36 +562,43 @@ def version_problems(lay: Layout) -> list[str]:
|
|||||||
бы объявить каталог приведённым к формату, шагов которого никто не делал.
|
бы объявить каталог приведённым к формату, шагов которого никто не делал.
|
||||||
Заводит число `init`, двигает — операция `upgrade` скилла.
|
Заводит число `init`, двигает — операция `upgrade` скилла.
|
||||||
"""
|
"""
|
||||||
path = lay.root / CONFIG_NAME
|
path = lay.project / CONFIG_NAME
|
||||||
# Прежний дом (`docs/.pm.json`) версии не знает, поэтому спрашиваем строго
|
|
||||||
# свой файл: «конфиг нашёлся» и «версия объявлена» это разные события.
|
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return [f"нет {path} — версия формата задач не объявлена."
|
legacy = conf.legacy_files(lay.project, lay.root)
|
||||||
f" Заведи файл с «{VERSION_KEY}»: {FORMAT_VERSION} (журнал версий —"
|
if legacy:
|
||||||
f" references/changelog.md скилла av-dev:task-track)"]
|
return [f"нет {path}, а прежняя раскладка на месте"
|
||||||
# Что число целое, уже проверил `_validate_config` — иначе сюда не дошли бы
|
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` значит ровно «ключ есть».
|
# вовсе (код 3). Здесь `isinstance` значит ровно «ключ есть».
|
||||||
got = lay.cfg.get(VERSION_KEY)
|
got = conf.version(lay.full)
|
||||||
if not isinstance(got, int):
|
if got is None:
|
||||||
return [f"{path}: нет ключа «{VERSION_KEY}» — версия формата не объявлена,"
|
return [f"{path}: нет ключа «{VERSION_KEY}» — версия раскладки не"
|
||||||
f" текущая {FORMAT_VERSION}"]
|
f" объявлена, текущая {FORMAT_VERSION}"]
|
||||||
if got < FORMAT_VERSION:
|
if got < FORMAT_VERSION:
|
||||||
return [f"каталог приведён к формату версии {got}, текущая —"
|
return [f"проект приведён к раскладке версии {got}, текущая —"
|
||||||
f" {FORMAT_VERSION}: нужно повышение по журналу"
|
f" {FORMAT_VERSION}: нужно повышение по журналу"
|
||||||
f" (скилл av-dev:task-track, операция upgrade)"]
|
f" (скилл av-dev:doc-canon, операция upgrade)"]
|
||||||
if got > FORMAT_VERSION:
|
if got > FORMAT_VERSION:
|
||||||
return [f"каталог приведён к формату версии {got}, а скрипт знает"
|
return [f"проект приведён к раскладке версии {got}, а скрипт знает"
|
||||||
f" {FORMAT_VERSION}: устарел плагин, обнови маркетплейс"]
|
f" {FORMAT_VERSION}: устарел плагин, обнови маркетплейс"]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def looks_like_tasks(p: Path) -> bool:
|
def looks_like_tasks(p: Path, names: dict | None = None) -> bool:
|
||||||
if (p / CONFIG_NAME).is_file():
|
"""Каталог задач узнаётся индексом, а не служебным файлом.
|
||||||
return True
|
|
||||||
try: # индекс мог быть переименован через конфиг
|
Служебный файл теперь лежит в корне проекта и о каталоге говорит ключом
|
||||||
name = load_config(p).get("backlog", DEFAULTS["backlog"])
|
`[tasks] dir`; узнавать каталог по нему значило бы объявить его задачами
|
||||||
except Env:
|
ровно там, куда указывает ключ, — даже если по этому пути пусто.
|
||||||
name = DEFAULTS["backlog"]
|
|
||||||
|
Имя индекса берётся из настроек: проект вправе назвать его по-своему, и
|
||||||
|
поиск по умолчанию не нашёл бы переименованного каталога вовсе.
|
||||||
|
"""
|
||||||
|
name = (names or {}).get("backlog") or DEFAULTS["backlog"]
|
||||||
return (p / name).is_file()
|
return (p / name).is_file()
|
||||||
|
|
||||||
|
|
||||||
@@ -619,33 +606,51 @@ def resolve_layout(explicit: str | None) -> Layout:
|
|||||||
"""Каталог задач для команд, кроме init.
|
"""Каталог задач для команд, кроме init.
|
||||||
|
|
||||||
Цепочка разрешения: явный `--dir` (обязан быть внутри рабочего каталога) →
|
Цепочка разрешения: явный `--dir` (обязан быть внутри рабочего каталога) →
|
||||||
`.tasks.json` или умолчания вверх от текущего каталога. Указатель в
|
ключ `[tasks] dir` из `.av-dev.toml` в корне → умолчание `tasks/` вверх от
|
||||||
`CLAUDE.md` проекта — звено между ними, но читает его агент и передаёт
|
текущего каталога. Указатель в `CLAUDE.md` проекта — звено между первым и
|
||||||
сюда `--dir`: скрипт не разбирает чужую документацию.
|
вторым, но читает его агент и передаёт сюда `--dir`: скрипт не разбирает
|
||||||
|
чужую документацию.
|
||||||
"""
|
"""
|
||||||
|
here = Path.cwd().resolve()
|
||||||
|
project = conf.find_root(here)
|
||||||
|
full = load_config(project) if project else {}
|
||||||
|
names = tasks_section(full)
|
||||||
|
|
||||||
if explicit:
|
if explicit:
|
||||||
root = Path(explicit)
|
root = Path(explicit)
|
||||||
if not dir_within_cwd(root):
|
if not dir_within_cwd(root):
|
||||||
raise Env(f"--dir вне рабочего каталога: {explicit}")
|
raise Env(f"--dir вне рабочего каталога: {explicit}")
|
||||||
if not looks_like_tasks(root):
|
if not looks_like_tasks(root, names):
|
||||||
raise Env(f"задач нет в «{explicit}»;"
|
raise Env(f"задач нет в «{explicit}»;"
|
||||||
f" новый проект — tasks.py init --dir {explicit}")
|
f" новый проект — tasks.py init --dir {explicit}")
|
||||||
return Layout(root, load_config(root))
|
return Layout(root, names, project or root.resolve(), full)
|
||||||
here = Path.cwd().resolve()
|
|
||||||
|
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 base in (here, *here.parents):
|
||||||
for candidate in (base, base / "tasks", base / "docs/tasks", base / "doc/tasks"):
|
for candidate in (base, base / DEFAULT_DIR, base / "docs/tasks", base / "doc/tasks"):
|
||||||
if looks_like_tasks(candidate):
|
if looks_like_tasks(candidate, names):
|
||||||
try:
|
return Layout(relative_if_inside(candidate, here), names,
|
||||||
rel = candidate.relative_to(here)
|
project or base, full)
|
||||||
except ValueError:
|
|
||||||
rel = candidate
|
|
||||||
return Layout(rel if str(rel) != "." else candidate, load_config(candidate))
|
|
||||||
if (base / ".git").exists():
|
if (base / ".git").exists():
|
||||||
break # выше корня репозитория не ищем
|
break # выше корня репозитория не ищем
|
||||||
raise Env("каталог задач не найден: ни --dir, ни tasks/ вверх от"
|
raise Env(f"каталог задач не найден: ни --dir, ни ключ [tasks] {DIR_KEY} в"
|
||||||
f" {here}. Путь всегда tasks/ в корне репозитория; прежний"
|
f" {CONFIG_NAME}, ни {DEFAULT_DIR}/ вверх от {here}."
|
||||||
" docs/tasks переезжает по записи 11 журнала версий канона,"
|
f" Новый проект — tasks.py init --dir {DEFAULT_DIR}")
|
||||||
" новый проект — tasks.py init --dir tasks")
|
|
||||||
|
|
||||||
|
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:
|
for p in problems:
|
||||||
print(f"КОНФИГ {p}")
|
print(f"КОНФИГ {p}")
|
||||||
print("\nсперва конфиг: пока он мимо, всё остальное диагностируется ложно"
|
print("\nсперва конфиг: пока он мимо, всё остальное диагностируется ложно"
|
||||||
f" (правь {config_home(lay.root) or lay.root / CONFIG_NAME}"
|
f" (правь {config_home(lay) or lay.project / CONFIG_NAME}"
|
||||||
f" или переименуй файлы)")
|
f" или переименуй файлы)")
|
||||||
return EXIT_ENV
|
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],
|
def init_files(lay: Layout, sections: list[str], roadmap_sections: list[str],
|
||||||
cfg: dict) -> dict[Path, str]:
|
cfg: dict) -> dict[Path, str]:
|
||||||
out: dict[Path, str] = {}
|
out: dict[Path, str] = {}
|
||||||
# Файл заводится всегда, даже когда все имена умолчательные: в нём живёт
|
# Служебный файл заводится всегда, даже когда все имена умолчательные: в нём
|
||||||
# версия формата, а версия — не настройка, от которой можно отказаться.
|
# живёт версия раскладки, а версия — не настройка, от которой можно
|
||||||
#
|
# отказаться. Файл уже есть (проект под каноном, заводят только задачи) —
|
||||||
# Пишем всегда в свой `.tasks.json`, даже когда рядом живёт `docs/.pm.json`:
|
# он не перезаписывается: комментарии в нём принадлежат человеку. Тогда
|
||||||
# дом настроек принадлежит этому плагину, а `docs/` — другому, и его в
|
# недостающие ключи секции дописываются построчно, и делает это `cmd_init`
|
||||||
# проекте может не быть. load_config читает свой файл первым, так что
|
# после записи файлов, потому что правка идёт по живому файлу, а не планом.
|
||||||
# записанное сюда и прочитается отсюда.
|
if not (lay.project / CONFIG_NAME).is_file():
|
||||||
out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False,
|
out[lay.project / CONFIG_NAME] = conf.skeleton(FORMAT_VERSION, tasks=cfg)
|
||||||
indent=2) + "\n"
|
|
||||||
out[lay.index("backlog")] = (
|
out[lay.index("backlog")] = (
|
||||||
"# Беклог\n\n"
|
"# Беклог\n\n"
|
||||||
f"Что **можно взять**. Одна задача = один файл `{lay.cfg['items']}/<slug>.md`\n"
|
f"Что **можно взять**. Одна задача = один файл `{lay.cfg['items']}/<slug>.md`\n"
|
||||||
@@ -2670,8 +2674,18 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
|
|||||||
# Имена частей — следом и только те, что названы явно: умолчание, записанное
|
# Имена частей — следом и только те, что названы явно: умолчание, записанное
|
||||||
# в файл, стало бы вторым домом для того же имени. В раскладку версия не
|
# в файл, стало бы вторым домом для того же имени. В раскладку версия не
|
||||||
# идёт — `Layout` про имена, и число среди имён там ничего не значит.
|
# идёт — `Layout` про имена, и число среди имён там ничего не значит.
|
||||||
cfg = {VERSION_KEY: FORMAT_VERSION, **names}
|
# Каталог задач называется ключом `dir`, если он не умолчательный: без него
|
||||||
lay = Layout(root, names)
|
# `.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():
|
if lay.index("backlog").exists():
|
||||||
raise Usage(f"{lay.index('backlog')} уже есть — каталог задач заведён")
|
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():
|
for path, text in init_files(lay, sections, roadmap_sections, cfg).items():
|
||||||
plan.file(path, text)
|
plan.file(path, text)
|
||||||
plan.commit()
|
plan.commit()
|
||||||
|
added = conf.merge_section(project, "tasks", cfg) if cfg else []
|
||||||
print(f"каталог задач заведён: {root}")
|
print(f"каталог задач заведён: {root}")
|
||||||
print(f" секции беклога: {', '.join(sections)};"
|
print(f" секции беклога: {', '.join(sections)};"
|
||||||
f" секции роадмапа канонические: {', '.join(roadmap_sections)}")
|
f" секции роадмапа канонические: {', '.join(roadmap_sections)}")
|
||||||
what = ("версия формата и имена частей записаны" if names
|
if added:
|
||||||
else "версия формата записана")
|
print(f" дописано в [tasks] {project / CONFIG_NAME}: {', '.join(added)}")
|
||||||
print(f" {what} в {root / CONFIG_NAME}: формат {FORMAT_VERSION}")
|
print(f" версия раскладки в {project / CONFIG_NAME}: {FORMAT_VERSION}")
|
||||||
return EXIT_OK
|
return EXIT_OK
|
||||||
|
|
||||||
|
|
||||||
@@ -2999,7 +3014,8 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
|
|||||||
root = Path(pl["target"])
|
root = Path(pl["target"])
|
||||||
if not dir_within_cwd(root):
|
if not dir_within_cwd(root):
|
||||||
raise Usage(f"target вне рабочего каталога: {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)
|
sections = pl.get("sections_backlog") or uniq_sections(DEFAULT_SECTIONS)
|
||||||
roadmap_sections = pl.get("sections_roadmap") or uniq_sections(DEFAULT_ROADMAP_SECTIONS)
|
roadmap_sections = pl.get("sections_roadmap") or uniq_sections(DEFAULT_ROADMAP_SECTIONS)
|
||||||
known_sections = {s.lower() for s in sections}
|
known_sections = {s.lower() for s in sections}
|
||||||
@@ -3058,8 +3074,7 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
|
|||||||
# Версия та же, что у `init`: каталог выводится из чужой раскладки сегодня и
|
# Версия та же, что у `init`: каталог выводится из чужой раскладки сегодня и
|
||||||
# сегодняшним форматом, сколько бы лет ни было тому, из чего он выведен.
|
# сегодняшним форматом, сколько бы лет ни было тому, из чего он выведен.
|
||||||
# Имён частей здесь нет — адаптация раскладывает всё по умолчаниям.
|
# Имён частей здесь нет — адаптация раскладывает всё по умолчаниям.
|
||||||
for path, text in init_files(lay, sections, roadmap_sections,
|
for path, text in init_files(lay, sections, roadmap_sections, {}).items():
|
||||||
{VERSION_KEY: FORMAT_VERSION}).items():
|
|
||||||
wr.file(path, text)
|
wr.file(path, text)
|
||||||
backlog_lines = init_files(lay, sections, roadmap_sections, {})[lay.index("backlog")].splitlines()
|
backlog_lines = init_files(lay, sections, roadmap_sections, {})[lay.index("backlog")].splitlines()
|
||||||
roadmap_lines = init_files(lay, sections, roadmap_sections, {})[lay.index("roadmap")].splitlines()
|
roadmap_lines = init_files(lay, sections, roadmap_sections, {})[lay.index("roadmap")].splitlines()
|
||||||
|
|||||||
@@ -56,8 +56,11 @@ OWNERS = {
|
|||||||
# Журналы: описывают прошлые состояния и задним числом не переписываются.
|
# Журналы: описывают прошлые состояния и задним числом не переписываются.
|
||||||
# Адрес, верный на момент записи, здесь останется навсегда, и это не дрейф.
|
# Адрес, верный на момент записи, здесь останется навсегда, и это не дрейф.
|
||||||
JOURNALS = {
|
JOURNALS = {
|
||||||
"av-dev/skills/doc-canon/references/changelog.md": "журнал версий канона",
|
"av-dev/skills/doc-canon/references/changelog.md": "журнал версий раскладки",
|
||||||
"av-dev/skills/task-track/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": "журнал решений",
|
"DECISIONS.md": "журнал решений",
|
||||||
"HISTORY.md": "журнал работ",
|
"HISTORY.md": "журнал работ",
|
||||||
"NOTES.md": "рабочие заметки",
|
"NOTES.md": "рабочие заметки",
|
||||||
|
|||||||
Reference in New Issue
Block a user