канон 13: файл версии зовётся по владельцу, у задач появилась своя версия формата
Имя `.pm.json` пережило плагин `av-dev-pm` на два месяца и указывало в пустоту. Правило, которое из этого вынуто: имя служебного файла — имя плагина, который его завёл, и по нему же владельца узнают. - `docs/.pm.json` → `docs/.docs.json`, запись 13 журнала. Прежнее имя docs.py не читает намеренно: по этому числу upgrade решает, какие записи применять, и два дома разъехались бы молча ровно там, где это дороже всего. Вместо совместимости — узнавание: check видит старый файл и печатает готовую git mv - у каталога задач появилась своя версия формата — ключ `tasks` в `.tasks.json`, свой журнал версий и своё повышение. До сих пор её не было вовсе, хотя docs.py в комментарии уверенно на неё ссылался: описание опережало механику ровно так, как сказано в решении 195 - число своё, а не копия канонического: плагин ставится в одиночку, и у проекта без docs/ версии канона нет — сверять было бы не с чем - конфиг задач стал обязательным (init и adopt apply пишут его всегда), check сверяет число, `check --fix` его не приписывает: приписанное объявляло бы каталог приведённым к формату, шагов которого никто не делал - переезды 11 и 12 в новый журнал задним числом не переписаны — версия 1 велит догнать формат по журналу канона, называя признаки отставания поимённо (каталог в docs/tasks/, живой SPRINT.md) - запись 60 в DECISIONS со следствиями 200–203; отдельно разведено с решением F, где `.docs.json` отвергался как указатель путей: отвергнут был указатель, а не имя
This commit is contained in:
@@ -25,10 +25,19 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import NoReturn
|
||||
|
||||
CANON_VERSION = 12
|
||||
CANON_VERSION = 13
|
||||
|
||||
OK, DRIFT, USAGE, ENV, INTERNAL = 0, 1, 2, 3, 4
|
||||
|
||||
# Дом версии канона и путей, нужных проверкам. Имя — от плагина, который файл
|
||||
# завёл: настройки канона документов ведёт `av-dev-docs`, и файл называется по
|
||||
# нему. Прежнее имя досталось от `av-dev-pm` — плагина, который распался на
|
||||
# четыре и которого больше нет; читать его скрипт не умеет намеренно, потому что
|
||||
# два дома для версии канона расходятся молча, а переименование стоит одну
|
||||
# команду и названо записью 13 журнала.
|
||||
CONFIG = "docs/.docs.json"
|
||||
LEGACY_CONFIG = "docs/.pm.json"
|
||||
|
||||
# --- Раскладка канона -------------------------------------------------------
|
||||
|
||||
# Документ канона: имя → (категория, на какой вопрос отвечает).
|
||||
@@ -59,7 +68,7 @@ DOCS = {
|
||||
"review": ("процессный", "настройка конвейера + журнал дефектов"),
|
||||
}
|
||||
|
||||
# Документ, обязательный только при условии: имя → (ключ .pm.json, категория,
|
||||
# Документ, обязательный только при условии: имя → (ключ .docs.json, категория,
|
||||
# пояснение).
|
||||
CONDITIONAL_DOCS = {
|
||||
"database": ("migrations", "источник", "схема хранилища и настройки"),
|
||||
@@ -68,7 +77,7 @@ CONDITIONAL_DOCS = {
|
||||
# Обязательные файлы вне раскладки docs/.
|
||||
REQUIRED = {
|
||||
"CLAUDE.md": "памятка агенту: инварианты с severity, команды, семантика гейта",
|
||||
"docs/.pm.json": "версия канона и пути, нужные проверкам",
|
||||
CONFIG: "версия канона и пути, нужные проверкам",
|
||||
}
|
||||
|
||||
# Файлы, которые документ-каталог обязан держать сверх README.md.
|
||||
@@ -77,15 +86,19 @@ DOC_EXTRA = {
|
||||
}
|
||||
|
||||
# Служебное в docs/ и каталог задач, оставшийся там от прежней раскладки. Формы
|
||||
# у них скрипт не проверяет, и по разным причинам: `.pm.json` не markdown, а
|
||||
# у них скрипт не проверяет, и по разным причинам: `.docs.json` не markdown, а
|
||||
# задачи **принадлежат другому плагину** — `av-dev-tasks`, со своим скриптом,
|
||||
# своим конфигом и своей версией формата.
|
||||
# своим конфигом и своей версией формата (её сторожит `tasks.py check`).
|
||||
#
|
||||
# Дом задач с версии 11 — `tasks/` в корне репозитория, то есть вне `docs/`
|
||||
# вовсе. `docs/tasks/` здесь терпится потому, что непереехавший проект не должен
|
||||
# получать «файл вне канона» вдобавок к записи журнала, которая и так велит ему
|
||||
# переехать. Внутрь скрипт не смотрит ни в том, ни в другом случае.
|
||||
NOT_DOCS = {".pm.json", "tasks"}
|
||||
#
|
||||
# Прежнее имя конфига терпится ровно за тем же: про переименование проект
|
||||
# слышит одну строку — от `check_required`, — а не две, из которых вторая ещё и
|
||||
# зовёт файл лишним.
|
||||
NOT_DOCS = {".docs.json", ".pm.json", "tasks"}
|
||||
|
||||
# Слоты, которых в каноне нет, — с адресом, куда уезжает содержимое. Имена,
|
||||
# совпадающие с темой, отсюда убраны намеренно: `docs/conventions.md` и
|
||||
@@ -251,15 +264,15 @@ def fail(code: int, msg: str) -> NoReturn:
|
||||
|
||||
|
||||
def read_config(root: Path, rep: Report) -> dict:
|
||||
path = root / "docs" / ".pm.json"
|
||||
path = root / CONFIG
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
fail(ENV, f"docs/.pm.json не разбирается: {exc}")
|
||||
fail(ENV, f"{CONFIG} не разбирается: {exc}")
|
||||
if not isinstance(data, dict):
|
||||
fail(ENV, "docs/.pm.json должен быть объектом")
|
||||
fail(ENV, f"{CONFIG} должен быть объектом")
|
||||
return data
|
||||
|
||||
|
||||
@@ -267,14 +280,14 @@ def read_config(root: Path, rep: Report) -> dict:
|
||||
|
||||
|
||||
def check_version(root: Path, cfg: dict, rep: Report) -> None:
|
||||
if not (root / "docs" / ".pm.json").exists():
|
||||
if not (root / CONFIG).exists():
|
||||
return # об отсутствии файла скажет check_required, второй раз не нужно
|
||||
if "canon" not in cfg:
|
||||
rep.error("в docs/.pm.json нет ключа canon — версия канона не объявлена")
|
||||
rep.error(f"в {CONFIG} нет ключа canon — версия канона не объявлена")
|
||||
return
|
||||
got = cfg["canon"]
|
||||
if not isinstance(got, int):
|
||||
rep.error(f"canon в docs/.pm.json должен быть целым числом, а не {got!r}")
|
||||
rep.error(f"canon в {CONFIG} должен быть целым числом, а не {got!r}")
|
||||
return
|
||||
if got < CANON_VERSION:
|
||||
rep.error(
|
||||
@@ -316,8 +329,21 @@ def doc_home(root: Path, name: str) -> tuple[Path | None, str | None]:
|
||||
|
||||
def check_required(root: Path, cfg: dict, rep: Report) -> None:
|
||||
for rel, what in REQUIRED.items():
|
||||
if not (root / rel).exists():
|
||||
rep.error(f"нет {rel} — {what}")
|
||||
if (root / rel).exists():
|
||||
continue
|
||||
# Файл под прежним именем — это не «нет файла», а незаконченный переезд,
|
||||
# и чинится он одной командой. Без этой ветки проект услышал бы «нет
|
||||
# версии канона» и пошёл заводить второй файл рядом с первым.
|
||||
if rel == CONFIG and (root / LEGACY_CONFIG).exists():
|
||||
rep.error(
|
||||
f"нет {rel} — {what}. Настройки лежат под прежним именем"
|
||||
f" {LEGACY_CONFIG} (от плагина av-dev-pm, которого больше нет):"
|
||||
f" `git mv {LEGACY_CONFIG} {rel}` — журнал канона, версия 13."
|
||||
f" Прежнее имя не читается, поэтому в этом прогоне всё"
|
||||
f" остальное проверено так, будто настроек нет вовсе"
|
||||
)
|
||||
continue
|
||||
rep.error(f"нет {rel} — {what}")
|
||||
|
||||
for name, (kind, what) in DOCS.items():
|
||||
home, complaint = doc_home(root, name)
|
||||
@@ -342,10 +368,10 @@ def check_required(root: Path, cfg: dict, rep: Report) -> None:
|
||||
rep.error(
|
||||
f"нет документа {name} (docs/{name}.md или docs/{name}/),"
|
||||
f" категория «{kind}» — {what}"
|
||||
f" (обязателен: в .pm.json объявлен {key})"
|
||||
f" (обязателен: в .docs.json объявлен {key})"
|
||||
)
|
||||
elif key not in cfg and home is None:
|
||||
rep.skip(f"{name} — в .pm.json нет ключа {key}, проверка неприменима")
|
||||
rep.skip(f"{name} — в .docs.json нет ключа {key}, проверка неприменима")
|
||||
|
||||
|
||||
def check_stray(root: Path, rep: Report) -> None:
|
||||
@@ -527,7 +553,7 @@ def changed_files(root: Path, base: str, rep: Report) -> list[str] | None:
|
||||
def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None:
|
||||
migrations = cfg.get("migrations")
|
||||
if not migrations:
|
||||
rep.skip("в .pm.json нет ключа migrations — сверка со схемой неприменима")
|
||||
rep.skip("в .docs.json нет ключа migrations — сверка со схемой неприменима")
|
||||
return
|
||||
if not base:
|
||||
rep.skip("база диффа не названа (--base) — сверка миграций со схемой не гонялась")
|
||||
|
||||
Reference in New Issue
Block a user