канон отпустил каталог задач: docs.py не зовёт tasks.py, конфиг разъехался
Пока владелец был один, docs/tasks/ числился слотом канона: docs.py требовал каталог, звал внутрь чужой скрипт подпроцессом и выдавал его дрейф за свой, а настройки задач жили ключом tasks в docs/.pm.json. Для проекта, поставившего только документы, всё это отказ на ровном месте — задач он не ведёт, и требовать их не за что. Раскол вскрыл это немедленно и молча: check_tasks искал tasks.py по пути parents[2]/tasks/scripts, то есть внутри своего плагина, и после переезда скатывался в ветку «скрипт не найден» на каждом прогоне. Проверка выглядела живой и не проверяла ничего. Теперь docs.py про задачи не говорит ни слова: check_tasks снят целиком, каталог остаётся в NOT_DOCS, его отсутствие дрейфом не считается. Канон резервирует место в docs/ и внутрь не смотрит. Дом настроек каталога задач вернулся в свой файл — <каталог>/.tasks.json. Прежний ключ tasks в docs/.pm.json читается, только когда своего файла нет, и скрипт говорит, куда его перенести; есть оба — побеждает свой, и об этом тоже говорится вслух. Порядок именно такой, потому что docs/ принадлежит другому плагину: дом настроек в чужом дереве это дом, которого у половины проектов нет. Заодно закрыта дыра, которую сам же и открыл первый вариант правки: битый docs/.pm.json ронял бы задачи даже при живом своём конфиге. Чужой файл здесь только повод для замечания, и его поломка не наша. Проверено на четырёх случаях — только чужой конфиг, оба, свой плюс битый чужой (код 0), только битый чужой (код 3, окружение). Канон повышен до версии 8 с записью, выполнимой upgrade. В ней названо и то, что легко потерять: раньше согласованность задач тянул за собой docs.py check, и проект, у которого в гейте стоял только он, обязан добавить второй шаг — иначе дрейф индексов перестанет ловиться молча. Гейт зелёный. Оба скрипта прогнаны: docs.py check на фикстуре про задачи не упоминает, tasks.py check код 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -624,10 +624,17 @@ python3 $tk adopt scan --from … | apply --plan … # разовая адап
|
||||
действительно новый, а перевод чужой раскладки делает `av-dev-docs:canon`.
|
||||
У скрипта поиск вверх по дереву ещё жив — он для непереведённых проектов, и
|
||||
полагаться на него скилл не должен: молча найденный чужой каталог это дрейф.
|
||||
- **Настройки живут в `docs/.pm.json`**, ключ `tasks`: **имена** файлов и
|
||||
заголовков, и только если они отличаются от умолчания. Один конфиг на весь
|
||||
канон, а не по одному на каталог. Неизвестный ключ — код 3 на любой команде,
|
||||
так что лишнее слово в этом объекте останавливает работу с задачами целиком.
|
||||
- **Настройки живут в `<каталог задач>/.tasks.json`** — свой файл у своего
|
||||
плагина: **имена** файлов и заголовков, и только если они отличаются от
|
||||
умолчания. Неизвестный ключ — код 3 на любой команде, так что лишнее слово в
|
||||
этом объекте останавливает работу с задачами целиком.
|
||||
|
||||
Дом именно свой, а не `docs/.pm.json`, потому что `docs/` принадлежит плагину
|
||||
канона: проект, поставивший учёт работ без него, каталога `docs/` не имеет
|
||||
вовсе. Прежний ключ `tasks` в `docs/.pm.json` читается, **только когда своего
|
||||
файла нет** — для проектов, заведённых до раскола плагинов; скрипт при этом
|
||||
говорит замечанием, куда его перенести. Есть оба — побеждает свой, и об этом
|
||||
тоже говорится вслух: молча выбранный из двух конфиг это дрейф.
|
||||
- **Секции беклога** берутся из заголовков `##` индекса как есть; их количество
|
||||
и названия — дело проекта (умолчание `Ядро` / `Инфра`). **В конфиге их нет** —
|
||||
второй список разошёлся бы с заголовками молча.
|
||||
|
||||
@@ -108,8 +108,8 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_NAME = ".tasks.json" # прежний дом настроек, читается для совместимости
|
||||
PM_CONFIG_REL = "../.pm.json" # текущий дом: docs/.pm.json, ключ "tasks"
|
||||
CONFIG_NAME = ".tasks.json" # дом настроек: свой файл в каталоге задач
|
||||
PM_CONFIG_REL = "../.pm.json" # прежний дом: docs/.pm.json, ключ "tasks"
|
||||
|
||||
EXIT_OK = 0
|
||||
EXIT_DRIFT = 1
|
||||
@@ -443,27 +443,43 @@ class Layout:
|
||||
def load_config(root: Path) -> dict:
|
||||
"""Настройки каталога задач.
|
||||
|
||||
Дом один — `docs/.pm.json`, ключ `tasks`: один конфиг на весь канон, а не по
|
||||
одному на каталог. Прежний `<tasks>/.tasks.json` читается, пока живы проекты,
|
||||
которые ещё не переехали; когда есть оба, побеждает `.pm.json`, и об этом
|
||||
говорится вслух, потому что молча выбранный из двух конфиг — это дрейф,
|
||||
который потом никто не объяснит.
|
||||
Дом — `<каталог задач>/.tasks.json`: **свой файл у своего плагина**. Ключ
|
||||
`tasks` в `docs/.pm.json` читается, пока живы проекты, заведённые до раскола
|
||||
плагинов, и только когда своего файла нет; когда есть оба, побеждает свой, и
|
||||
об этом говорится вслух — молча выбранный из двух конфиг это дрейф, который
|
||||
потом никто не объяснит.
|
||||
|
||||
Порядок именно такой, а не наоборот, потому что `docs/` принадлежит другому
|
||||
плагину. Проект, поставивший учёт задач без канона документов, каталога
|
||||
`docs/` не имеет вовсе, и дом настроек, лежащий в чужом дереве, был бы домом,
|
||||
которого у половины проектов нет.
|
||||
"""
|
||||
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 (root / CONFIG_NAME).is_file():
|
||||
print(f"ЗАМЕЧАНИЕ настройки взяты из {pm}; {root / CONFIG_NAME}"
|
||||
f" остался от прежней раскладки и не читается — удали его",
|
||||
file=sys.stderr)
|
||||
if section:
|
||||
print(f"ЗАМЕЧАНИЕ настройки взяты из ключа «tasks» в {pm} — это"
|
||||
f" прежний дом. Перенеси их в {path}: каталог docs/ ведёт"
|
||||
f" другой плагин, и его может не быть", file=sys.stderr)
|
||||
return _validate_config(section, pm)
|
||||
path = root / CONFIG_NAME
|
||||
if not path.is_file():
|
||||
return {}
|
||||
return _validate_config(_read_json(path), path)
|
||||
return {}
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict:
|
||||
@@ -500,14 +516,14 @@ def _validate_config(data: dict, path: Path) -> dict:
|
||||
def config_home(root: Path) -> Path | None:
|
||||
"""Откуда настройки читаются на самом деле — и куда, значит, слать чинить.
|
||||
|
||||
Порядок тот же, что в `load_config`: `docs/.pm.json` побеждает. Без этой
|
||||
функции сообщения об ошибке звали править `.tasks.json`, который при живом
|
||||
`.pm.json` вообще не читается.
|
||||
Порядок тот же, что в `load_config`: свой `.tasks.json` побеждает. Без этой
|
||||
функции сообщения об ошибке звали бы править файл, который не читается.
|
||||
"""
|
||||
path = root / CONFIG_NAME
|
||||
if path.is_file():
|
||||
return path
|
||||
pm = (root / PM_CONFIG_REL).resolve()
|
||||
if pm.is_file():
|
||||
return pm
|
||||
return root / CONFIG_NAME if (root / CONFIG_NAME).is_file() else None
|
||||
return pm if pm.is_file() else None
|
||||
|
||||
|
||||
def config_problems(lay: Layout) -> list[str]:
|
||||
@@ -2734,20 +2750,12 @@ def init_files(lay: Layout, sections: list[str], roadmap_sections: list[str],
|
||||
cfg: dict) -> dict[Path, str]:
|
||||
out: dict[Path, str] = {}
|
||||
if cfg:
|
||||
# Дом настроек один — `docs/.pm.json`, ключ `tasks`. Писать в
|
||||
# `.tasks.json` при живом `.pm.json` значит писать туда, откуда никто
|
||||
# не читает: load_config его в этом случае игнорирует.
|
||||
pm = (lay.root / PM_CONFIG_REL).resolve()
|
||||
if pm.is_file():
|
||||
data = _read_json(pm)
|
||||
section = data.get("tasks") or {}
|
||||
if not isinstance(section, dict):
|
||||
raise Env(f"{pm}: ключ «tasks» — ожидался объект с настройками")
|
||||
data["tasks"] = {**section, **cfg}
|
||||
out[pm] = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
||||
else:
|
||||
out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False,
|
||||
indent=2) + "\n"
|
||||
# Пишем всегда в свой `.tasks.json`, даже когда рядом живёт
|
||||
# `docs/.pm.json`: дом настроек принадлежит этому плагину, а `docs/` —
|
||||
# другому, и его в проекте может не быть. load_config читает свой файл
|
||||
# первым, так что записанное сюда и прочитается отсюда.
|
||||
out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False,
|
||||
indent=2) + "\n"
|
||||
out[lay.index("backlog")] = (
|
||||
"# Беклог\n\n"
|
||||
f"Что **можно взять**. Одна задача = один файл `{lay.cfg['items']}/<slug>.md`\n"
|
||||
@@ -2833,8 +2841,7 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
|
||||
print(f" секции беклога: {', '.join(sections)};"
|
||||
f" секции роадмапа канонические: {', '.join(roadmap_sections)}")
|
||||
if cfg:
|
||||
pm = (root / PM_CONFIG_REL).resolve()
|
||||
print(f" имена частей записаны в {pm if pm.is_file() else root / CONFIG_NAME}")
|
||||
print(f" имена частей записаны в {root / CONFIG_NAME}")
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user