конфиг: одна версия и один служебный файл, .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:
@@ -7,11 +7,11 @@
|
||||
ровно в одном из них за раз. `REJECTED.md` индексом не считается: он не говорит,
|
||||
где запись числится, он кладбище ушедшего.
|
||||
|
||||
Раскладка. Путь каталога — `tasks/` в корне репозитория, жёстко. Каталог
|
||||
принадлежит этому плагину, а не канону документов: `docs/` ведёт другой плагин, и
|
||||
проект, поставивший учёт работ без него, каталога `docs/` не имеет вовсе. Имена
|
||||
внутри и **версия формата** живут в `tasks/.tasks.json`; журнал версий —
|
||||
references/changelog.md рядом со скриптом.
|
||||
Раскладка. Путь каталога — `tasks/` в корне репозитория по умолчанию; другой
|
||||
называется ключом `[tasks] dir`. Каталог принадлежит этому скиллу, а не канону
|
||||
документов: учёт работ ведут и в проекте, который к канону не приведён. Имена
|
||||
частей и **версия раскладки** живут в `.av-dev.toml` в корне; журнал версий —
|
||||
references/changelog.md скилла doc-canon.
|
||||
|
||||
tasks/
|
||||
items/ задачи и цели файлами, <slug>.md
|
||||
@@ -102,32 +102,47 @@ goal | feature | fix | chore | research, по-английски, как и пр
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
CONFIG_NAME = ".tasks.json" # дом настроек и версии: свой файл в каталоге
|
||||
PM_CONFIG_REL = "../.pm.json" # прежний дом настроек: docs/.pm.json, ключ "tasks"
|
||||
|
||||
# Версия формата задач — **своя, а не канона документов**. Число живёт ключом
|
||||
# `tasks` в `.tasks.json`, журнал версий — references/changelog.md рядом со
|
||||
# скриптом, повышает его операция `upgrade` скилла `av-dev:task-track`.
|
||||
def _load_shared() -> ModuleType:
|
||||
"""Общий читатель `.av-dev.toml` — `shared/config.py` этого же плагина.
|
||||
|
||||
Путь считается от файла скрипта: зовут его из репозитория проекта, где
|
||||
дерева плагина в текущем каталоге нет.
|
||||
"""
|
||||
path = Path(__file__).resolve().parents[3] / "shared" / "config.py"
|
||||
spec = importlib.util.spec_from_file_location("avdev_config", path)
|
||||
if spec is None or spec.loader is None:
|
||||
print(f"ОТКАЗ: не читается {path} — общий читатель настроек;"
|
||||
f" переустанови плагин av-dev", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
conf = _load_shared()
|
||||
|
||||
CONFIG_NAME = conf.CONFIG_NAME # дом настроек и версии: `.av-dev.toml` в корне
|
||||
|
||||
# Версия раскладки — **одна на плагин**, и живёт она в `shared/config.py`.
|
||||
# Своей у каталога задач больше нет: пока плагинов было три и ставились они
|
||||
# порознь, проект мог иметь учёт работ без канона документов, и общее число
|
||||
# было бы домом, которого у половины проектов нет. Плагин один — довод ушёл, а
|
||||
# два числа вместо одного оставляли бы вопрос «по какому журналу повышать».
|
||||
#
|
||||
# Число именно своё, потому что плагин ставится в одиночку: проект, взявший учёт
|
||||
# работ без канона документов, каталога `docs/` не имеет вовсе, а значит не имеет
|
||||
# и версии канона — сверять было бы не с чем. Копия чужого числа в этом скрипте
|
||||
# была бы вторым домом для одной версии и разъехалась бы молча при обновлении
|
||||
# одного плагина без другого.
|
||||
#
|
||||
# Переезды каталога задач, случившиеся до появления этого числа (в корень —
|
||||
# канон 11, отмена спринтов — канон 12), задним числом сюда не переписаны: они
|
||||
# уже названы журналом канона, и второй перечень тех же шагов разошёлся бы с
|
||||
# первым. Версия 1 — формат на день её появления, что бы проекту ни пришлось
|
||||
# пройти до неё.
|
||||
FORMAT_VERSION = 1
|
||||
VERSION_KEY = "tasks"
|
||||
# Переезды каталога, случившиеся до слияния (в корень, отмена спринтов), задним
|
||||
# числом в журнал не переписаны: они названы прежними журналами, и второй
|
||||
# перечень тех же шагов разошёлся бы с первым.
|
||||
FORMAT_VERSION = conf.VERSION
|
||||
VERSION_KEY = conf.VERSION_KEY
|
||||
|
||||
EXIT_OK = 0
|
||||
EXIT_DRIFT = 1
|
||||
@@ -135,6 +150,11 @@ EXIT_USAGE = 2
|
||||
EXIT_ENV = 3
|
||||
EXIT_INTERNAL = 4
|
||||
|
||||
# Ключ `dir` в DEFAULTS не входит намеренно: он говорит, **где** каталог, а не
|
||||
# как названы его части, и в `Layout` (тот про имена внутри) ему делать нечего.
|
||||
DIR_KEY = "dir"
|
||||
DEFAULT_DIR = "tasks"
|
||||
|
||||
DEFAULTS = {
|
||||
"items": "items",
|
||||
"backlog": "BACKLOG.md",
|
||||
@@ -435,9 +455,14 @@ class Layout:
|
||||
"""Каталог задач и имена его частей. Всё настраивается: у соседнего проекта
|
||||
может быть другой подкаталог и другие имена индексов, а семантика та же."""
|
||||
|
||||
def __init__(self, root: Path, cfg: dict):
|
||||
def __init__(self, root: Path, cfg: dict, project: Path | None = None,
|
||||
full: dict | None = None):
|
||||
self.root = root
|
||||
self.cfg = {**DEFAULTS, **cfg}
|
||||
# Корень проекта — там, где лежит `.av-dev.toml`. Он нужен отдельно от
|
||||
# каталога задач: версия объявлена в корне, а имена частей — внутри.
|
||||
self.project = project or root
|
||||
self.full = full or {}
|
||||
self.cfg = {**DEFAULTS, **{k: v for k, v in cfg.items() if k != DIR_KEY}}
|
||||
self.items = root / self.cfg["items"]
|
||||
|
||||
def index(self, kind: str) -> Path:
|
||||
@@ -451,64 +476,28 @@ class Layout:
|
||||
return ("backlog", "roadmap")
|
||||
|
||||
|
||||
def load_config(root: Path) -> dict:
|
||||
"""Настройки каталога задач и версия его формата.
|
||||
def load_config(project: Path) -> dict:
|
||||
"""Весь `.av-dev.toml` проекта. Секция задач берётся из него отдельно.
|
||||
|
||||
Дом — `<каталог задач>/.tasks.json`: **свой файл у своего плагина**. Ключ
|
||||
`tasks` в `docs/.pm.json` читается, пока живы проекты, заведённые до раскола
|
||||
плагинов, и только когда своего файла нет; когда есть оба, побеждает свой, и
|
||||
об этом говорится вслух — молча выбранный из двух конфиг это дрейф, который
|
||||
потом никто не объяснит.
|
||||
|
||||
Порядок именно такой, а не наоборот, потому что `docs/` принадлежит другому
|
||||
плагину. Проект, поставивший учёт задач без канона документов, каталога
|
||||
`docs/` не имеет вовсе, и дом настроек, лежащий в чужом дереве, был бы домом,
|
||||
которого у половины проектов нет.
|
||||
|
||||
Версия формата (ключ `tasks`) читается **только из своего файла**: прежний
|
||||
дом её не знал и знать не может, и молча выведенная из его отсутствия версия
|
||||
была бы догадкой о том, что чинится одной строкой.
|
||||
Дом настроек — **корень репозитория**, а не каталог задач: файл держит
|
||||
версию раскладки, которая одна на плагин, и ключ `[tasks] dir`, который
|
||||
говорит, где каталог лежит. Настройка внутри настраиваемого каталога не
|
||||
смогла бы сказать, где он.
|
||||
"""
|
||||
path = root / CONFIG_NAME
|
||||
pm = (root / PM_CONFIG_REL).resolve()
|
||||
if path.is_file():
|
||||
# Чужой конфиг здесь только повод для замечания, поэтому его поломка не
|
||||
# наша: битый `docs/.pm.json` не должен ронять задачи, у которых свой
|
||||
# файл на месте и читается.
|
||||
try:
|
||||
stale = pm.is_file() and isinstance(_read_json(pm).get("tasks"), dict)
|
||||
except Env:
|
||||
stale = False
|
||||
if stale:
|
||||
print(f"ЗАМЕЧАНИЕ настройки взяты из {path}; ключ «tasks» в {pm}"
|
||||
f" остался от прежней раскладки и не читается — убери его",
|
||||
file=sys.stderr)
|
||||
return _validate_config(_read_json(path), path)
|
||||
if pm.is_file():
|
||||
data = _read_json(pm)
|
||||
section = data.get("tasks", {})
|
||||
if not isinstance(section, dict):
|
||||
raise Env(f"{pm}: ключ «tasks» — ожидался объект с настройками")
|
||||
if section:
|
||||
print(f"ЗАМЕЧАНИЕ настройки взяты из ключа «tasks» в {pm} — это"
|
||||
f" прежний дом. Перенеси их в {path}: каталог docs/ ведёт"
|
||||
f" другой плагин, и его может не быть", file=sys.stderr)
|
||||
return _validate_config(section, pm)
|
||||
return {}
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict:
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as e:
|
||||
raise Env(f"{path}: не разбирается как JSON — {e}") from e
|
||||
if not isinstance(data, dict):
|
||||
raise Env(f"{path}: ожидался объект с настройками")
|
||||
data = conf.read(project)
|
||||
except conf.ConfigError as e:
|
||||
raise Env(str(e)) from e
|
||||
_validate_config(conf.section(data, "tasks"), project / CONFIG_NAME)
|
||||
return data
|
||||
|
||||
|
||||
def tasks_section(full: dict) -> dict:
|
||||
return conf.section(full, "tasks")
|
||||
|
||||
|
||||
def _validate_config(data: dict, path: Path) -> dict:
|
||||
unknown = set(data) - set(DEFAULTS) - {VERSION_KEY}
|
||||
unknown = set(data) - set(DEFAULTS) - {DIR_KEY}
|
||||
# Ключ «plan» был домом оглавления целей до того, как файл стал ROADMAP.md.
|
||||
# Без этой ветки проект со старым конфигом получал бы «неизвестный ключ» и
|
||||
# искал опечатку там, где на самом деле переименование канона.
|
||||
@@ -518,19 +507,12 @@ def _validate_config(data: dict, path: Path) -> dict:
|
||||
f" av-dev:doc-canon (upgrade), а не правь ключ в одиночку:"
|
||||
f" файл и ссылки на него переезжают вместе с ним")
|
||||
if unknown:
|
||||
known = sorted({*DEFAULTS, VERSION_KEY})
|
||||
raise Env(f"{path}: неизвестные ключи: {', '.join(sorted(unknown))}"
|
||||
f" (известны: {', '.join(known)})")
|
||||
# Версия — единственный ключ-число: остальные это имена файлов и заголовков.
|
||||
# Битое число тут останавливает работу целиком (код 3), а не идёт дрейфом,
|
||||
# потому что «на какой версии формата каталог» решает, чему верить дальше.
|
||||
got = data.get(VERSION_KEY)
|
||||
if VERSION_KEY in data and (isinstance(got, bool) or not isinstance(got, int)):
|
||||
raise Env(f"{path}: ключ «{VERSION_KEY}» — версия формата задач,"
|
||||
f" ожидалось целое число, а не {got!r}")
|
||||
known = sorted({*DEFAULTS, DIR_KEY})
|
||||
raise Env(f"{path}: неизвестные ключи в секции [tasks]:"
|
||||
f" {', '.join(sorted(unknown))} (известны: {', '.join(known)})")
|
||||
# Версия раскладки лежит ключом верхнего уровня и проверяется общим
|
||||
# читателем: здесь судится только секция задач, и все её ключи — строки.
|
||||
for key, value in data.items():
|
||||
if key == VERSION_KEY:
|
||||
continue
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise Env(f"{path}: ключ «{key}» — ожидалась непустая строка")
|
||||
if key in PATH_KEYS and (value.startswith("/") or ".." in Path(value).parts):
|
||||
@@ -538,17 +520,15 @@ def _validate_config(data: dict, path: Path) -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def config_home(root: Path) -> Path | None:
|
||||
def config_home(lay: Layout) -> Path | None:
|
||||
"""Откуда настройки читаются на самом деле — и куда, значит, слать чинить.
|
||||
|
||||
Порядок тот же, что в `load_config`: свой `.tasks.json` побеждает. Без этой
|
||||
функции сообщения об ошибке звали бы править файл, который не читается.
|
||||
Дом один — `.av-dev.toml` в корне проекта; None значит «файла нет, работаем
|
||||
на умолчаниях». Без этой функции сообщения об ошибке звали бы править файл,
|
||||
которого нет.
|
||||
"""
|
||||
path = root / CONFIG_NAME
|
||||
if path.is_file():
|
||||
return path
|
||||
pm = (root / PM_CONFIG_REL).resolve()
|
||||
return pm if pm.is_file() else None
|
||||
path = lay.project / CONFIG_NAME
|
||||
return path if path.is_file() else None
|
||||
|
||||
|
||||
def config_problems(lay: Layout) -> list[str]:
|
||||
@@ -558,7 +538,7 @@ def config_problems(lay: Layout) -> list[str]:
|
||||
check обвинять невиновных: «ссылка на несуществующий файл», хотя файл на
|
||||
месте, а мимо смотрит конфиг.
|
||||
"""
|
||||
where = str(config_home(lay.root) or "умолчания (конфига нет)")
|
||||
where = str(config_home(lay) or "умолчания (конфига нет)")
|
||||
out = []
|
||||
if not lay.items.is_dir():
|
||||
out.append(f"{where}: items = «{lay.cfg['items']}» → {lay.items} — каталога нет")
|
||||
@@ -582,36 +562,43 @@ def version_problems(lay: Layout) -> list[str]:
|
||||
бы объявить каталог приведённым к формату, шагов которого никто не делал.
|
||||
Заводит число `init`, двигает — операция `upgrade` скилла.
|
||||
"""
|
||||
path = lay.root / CONFIG_NAME
|
||||
# Прежний дом (`docs/.pm.json`) версии не знает, поэтому спрашиваем строго
|
||||
# свой файл: «конфиг нашёлся» и «версия объявлена» это разные события.
|
||||
path = lay.project / CONFIG_NAME
|
||||
if not path.is_file():
|
||||
return [f"нет {path} — версия формата задач не объявлена."
|
||||
f" Заведи файл с «{VERSION_KEY}»: {FORMAT_VERSION} (журнал версий —"
|
||||
f" references/changelog.md скилла av-dev:task-track)"]
|
||||
# Что число целое, уже проверил `_validate_config` — иначе сюда не дошли бы
|
||||
legacy = conf.legacy_files(lay.project, lay.root)
|
||||
if legacy:
|
||||
return [f"нет {path}, а прежняя раскладка на месте"
|
||||
f" ({', '.join(legacy)}): перенеси настройки и удали старые"
|
||||
f" файлы операцией upgrade скилла av-dev:doc-canon"]
|
||||
return [f"нет {path} — версия раскладки не объявлена."
|
||||
f" Заведи файл с «{VERSION_KEY} = {FORMAT_VERSION}» (журнал"
|
||||
f" версий — references/changelog.md скилла av-dev:doc-canon)"]
|
||||
# Что число целое, уже проверил общий читатель — иначе сюда не дошли бы
|
||||
# вовсе (код 3). Здесь `isinstance` значит ровно «ключ есть».
|
||||
got = lay.cfg.get(VERSION_KEY)
|
||||
if not isinstance(got, int):
|
||||
return [f"{path}: нет ключа «{VERSION_KEY}» — версия формата не объявлена,"
|
||||
f" текущая {FORMAT_VERSION}"]
|
||||
got = conf.version(lay.full)
|
||||
if got is None:
|
||||
return [f"{path}: нет ключа «{VERSION_KEY}» — версия раскладки не"
|
||||
f" объявлена, текущая {FORMAT_VERSION}"]
|
||||
if got < FORMAT_VERSION:
|
||||
return [f"каталог приведён к формату версии {got}, текущая —"
|
||||
return [f"проект приведён к раскладке версии {got}, текущая —"
|
||||
f" {FORMAT_VERSION}: нужно повышение по журналу"
|
||||
f" (скилл av-dev:task-track, операция upgrade)"]
|
||||
f" (скилл av-dev:doc-canon, операция upgrade)"]
|
||||
if got > FORMAT_VERSION:
|
||||
return [f"каталог приведён к формату версии {got}, а скрипт знает"
|
||||
return [f"проект приведён к раскладке версии {got}, а скрипт знает"
|
||||
f" {FORMAT_VERSION}: устарел плагин, обнови маркетплейс"]
|
||||
return []
|
||||
|
||||
|
||||
def looks_like_tasks(p: Path) -> bool:
|
||||
if (p / CONFIG_NAME).is_file():
|
||||
return True
|
||||
try: # индекс мог быть переименован через конфиг
|
||||
name = load_config(p).get("backlog", DEFAULTS["backlog"])
|
||||
except Env:
|
||||
name = DEFAULTS["backlog"]
|
||||
def looks_like_tasks(p: Path, names: dict | None = None) -> bool:
|
||||
"""Каталог задач узнаётся индексом, а не служебным файлом.
|
||||
|
||||
Служебный файл теперь лежит в корне проекта и о каталоге говорит ключом
|
||||
`[tasks] dir`; узнавать каталог по нему значило бы объявить его задачами
|
||||
ровно там, куда указывает ключ, — даже если по этому пути пусто.
|
||||
|
||||
Имя индекса берётся из настроек: проект вправе назвать его по-своему, и
|
||||
поиск по умолчанию не нашёл бы переименованного каталога вовсе.
|
||||
"""
|
||||
name = (names or {}).get("backlog") or DEFAULTS["backlog"]
|
||||
return (p / name).is_file()
|
||||
|
||||
|
||||
@@ -619,33 +606,51 @@ def resolve_layout(explicit: str | None) -> Layout:
|
||||
"""Каталог задач для команд, кроме init.
|
||||
|
||||
Цепочка разрешения: явный `--dir` (обязан быть внутри рабочего каталога) →
|
||||
`.tasks.json` или умолчания вверх от текущего каталога. Указатель в
|
||||
`CLAUDE.md` проекта — звено между ними, но читает его агент и передаёт
|
||||
сюда `--dir`: скрипт не разбирает чужую документацию.
|
||||
ключ `[tasks] dir` из `.av-dev.toml` в корне → умолчание `tasks/` вверх от
|
||||
текущего каталога. Указатель в `CLAUDE.md` проекта — звено между первым и
|
||||
вторым, но читает его агент и передаёт сюда `--dir`: скрипт не разбирает
|
||||
чужую документацию.
|
||||
"""
|
||||
here = Path.cwd().resolve()
|
||||
project = conf.find_root(here)
|
||||
full = load_config(project) if project else {}
|
||||
names = tasks_section(full)
|
||||
|
||||
if explicit:
|
||||
root = Path(explicit)
|
||||
if not dir_within_cwd(root):
|
||||
raise Env(f"--dir вне рабочего каталога: {explicit}")
|
||||
if not looks_like_tasks(root):
|
||||
if not looks_like_tasks(root, names):
|
||||
raise Env(f"задач нет в «{explicit}»;"
|
||||
f" новый проект — tasks.py init --dir {explicit}")
|
||||
return Layout(root, load_config(root))
|
||||
here = Path.cwd().resolve()
|
||||
return Layout(root, names, project or root.resolve(), full)
|
||||
|
||||
if project:
|
||||
candidate = project / names.get(DIR_KEY, DEFAULT_DIR)
|
||||
if looks_like_tasks(candidate, names):
|
||||
return Layout(relative_if_inside(candidate, here), names, project, full)
|
||||
|
||||
# Проект без `.av-dev.toml` — учёт работ ведут и до того, как канон заведён.
|
||||
# Тогда каталог ищется умолчанием вверх, а версия объявится на `adopt`.
|
||||
for base in (here, *here.parents):
|
||||
for candidate in (base, base / "tasks", base / "docs/tasks", base / "doc/tasks"):
|
||||
if looks_like_tasks(candidate):
|
||||
try:
|
||||
rel = candidate.relative_to(here)
|
||||
except ValueError:
|
||||
rel = candidate
|
||||
return Layout(rel if str(rel) != "." else candidate, load_config(candidate))
|
||||
for candidate in (base, base / DEFAULT_DIR, base / "docs/tasks", base / "doc/tasks"):
|
||||
if looks_like_tasks(candidate, names):
|
||||
return Layout(relative_if_inside(candidate, here), names,
|
||||
project or base, full)
|
||||
if (base / ".git").exists():
|
||||
break # выше корня репозитория не ищем
|
||||
raise Env("каталог задач не найден: ни --dir, ни tasks/ вверх от"
|
||||
f" {here}. Путь всегда tasks/ в корне репозитория; прежний"
|
||||
" docs/tasks переезжает по записи 11 журнала версий канона,"
|
||||
" новый проект — tasks.py init --dir tasks")
|
||||
raise Env(f"каталог задач не найден: ни --dir, ни ключ [tasks] {DIR_KEY} в"
|
||||
f" {CONFIG_NAME}, ни {DEFAULT_DIR}/ вверх от {here}."
|
||||
f" Новый проект — tasks.py init --dir {DEFAULT_DIR}")
|
||||
|
||||
|
||||
def relative_if_inside(path: Path, here: Path) -> Path:
|
||||
"""Путь покороче для сообщений, если каталог лежит под текущим."""
|
||||
try:
|
||||
rel = path.relative_to(here)
|
||||
except ValueError:
|
||||
return path
|
||||
return path if str(rel) == "." else rel
|
||||
|
||||
|
||||
# --- Чтение индексов ---
|
||||
@@ -1079,7 +1084,7 @@ def check(lay: Layout, fix: bool = False) -> int:
|
||||
for p in problems:
|
||||
print(f"КОНФИГ {p}")
|
||||
print("\nсперва конфиг: пока он мимо, всё остальное диагностируется ложно"
|
||||
f" (правь {config_home(lay.root) or lay.root / CONFIG_NAME}"
|
||||
f" (правь {config_home(lay) or lay.project / CONFIG_NAME}"
|
||||
f" или переименуй файлы)")
|
||||
return EXIT_ENV
|
||||
|
||||
@@ -2596,15 +2601,14 @@ def apply_fixes(lay: Layout) -> tuple[list[str], list[str]]:
|
||||
def init_files(lay: Layout, sections: list[str], roadmap_sections: list[str],
|
||||
cfg: dict) -> dict[Path, str]:
|
||||
out: dict[Path, str] = {}
|
||||
# Файл заводится всегда, даже когда все имена умолчательные: в нём живёт
|
||||
# версия формата, а версия — не настройка, от которой можно отказаться.
|
||||
#
|
||||
# Пишем всегда в свой `.tasks.json`, даже когда рядом живёт `docs/.pm.json`:
|
||||
# дом настроек принадлежит этому плагину, а `docs/` — другому, и его в
|
||||
# проекте может не быть. load_config читает свой файл первым, так что
|
||||
# записанное сюда и прочитается отсюда.
|
||||
out[lay.root / CONFIG_NAME] = json.dumps(cfg, ensure_ascii=False,
|
||||
indent=2) + "\n"
|
||||
# Служебный файл заводится всегда, даже когда все имена умолчательные: в нём
|
||||
# живёт версия раскладки, а версия — не настройка, от которой можно
|
||||
# отказаться. Файл уже есть (проект под каноном, заводят только задачи) —
|
||||
# он не перезаписывается: комментарии в нём принадлежат человеку. Тогда
|
||||
# недостающие ключи секции дописываются построчно, и делает это `cmd_init`
|
||||
# после записи файлов, потому что правка идёт по живому файлу, а не планом.
|
||||
if not (lay.project / CONFIG_NAME).is_file():
|
||||
out[lay.project / CONFIG_NAME] = conf.skeleton(FORMAT_VERSION, tasks=cfg)
|
||||
out[lay.index("backlog")] = (
|
||||
"# Беклог\n\n"
|
||||
f"Что **можно взять**. Одна задача = один файл `{lay.cfg['items']}/<slug>.md`\n"
|
||||
@@ -2670,8 +2674,18 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
|
||||
# Имена частей — следом и только те, что названы явно: умолчание, записанное
|
||||
# в файл, стало бы вторым домом для того же имени. В раскладку версия не
|
||||
# идёт — `Layout` про имена, и число среди имён там ничего не значит.
|
||||
cfg = {VERSION_KEY: FORMAT_VERSION, **names}
|
||||
lay = Layout(root, names)
|
||||
# Каталог задач называется ключом `dir`, если он не умолчательный: без него
|
||||
# `.av-dev.toml` не сможет сказать, где искать, и разрешение уедет на
|
||||
# умолчание — молча и в другой каталог.
|
||||
project = conf.find_root() or Path.cwd().resolve()
|
||||
cfg = dict(names)
|
||||
try:
|
||||
rel = root.resolve().relative_to(project).as_posix()
|
||||
except ValueError:
|
||||
raise Usage(f"каталог задач {root} вне проекта {project}") from None
|
||||
if rel != DEFAULT_DIR:
|
||||
cfg[DIR_KEY] = rel
|
||||
lay = Layout(root, names, project, load_config(project))
|
||||
if lay.index("backlog").exists():
|
||||
raise Usage(f"{lay.index('backlog')} уже есть — каталог задач заведён")
|
||||
|
||||
@@ -2692,12 +2706,13 @@ def cmd_init(root: Path, a: argparse.Namespace) -> int:
|
||||
for path, text in init_files(lay, sections, roadmap_sections, cfg).items():
|
||||
plan.file(path, text)
|
||||
plan.commit()
|
||||
added = conf.merge_section(project, "tasks", cfg) if cfg else []
|
||||
print(f"каталог задач заведён: {root}")
|
||||
print(f" секции беклога: {', '.join(sections)};"
|
||||
f" секции роадмапа канонические: {', '.join(roadmap_sections)}")
|
||||
what = ("версия формата и имена частей записаны" if names
|
||||
else "версия формата записана")
|
||||
print(f" {what} в {root / CONFIG_NAME}: формат {FORMAT_VERSION}")
|
||||
if added:
|
||||
print(f" дописано в [tasks] {project / CONFIG_NAME}: {', '.join(added)}")
|
||||
print(f" версия раскладки в {project / CONFIG_NAME}: {FORMAT_VERSION}")
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
@@ -2999,7 +3014,8 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
|
||||
root = Path(pl["target"])
|
||||
if not dir_within_cwd(root):
|
||||
raise Usage(f"target вне рабочего каталога: {root}")
|
||||
lay = Layout(root, {})
|
||||
project = conf.find_root() or Path.cwd().resolve()
|
||||
lay = Layout(root, {}, project, load_config(project))
|
||||
sections = pl.get("sections_backlog") or uniq_sections(DEFAULT_SECTIONS)
|
||||
roadmap_sections = pl.get("sections_roadmap") or uniq_sections(DEFAULT_ROADMAP_SECTIONS)
|
||||
known_sections = {s.lower() for s in sections}
|
||||
@@ -3058,8 +3074,7 @@ def cmd_adopt_apply(a: argparse.Namespace) -> int:
|
||||
# Версия та же, что у `init`: каталог выводится из чужой раскладки сегодня и
|
||||
# сегодняшним форматом, сколько бы лет ни было тому, из чего он выведен.
|
||||
# Имён частей здесь нет — адаптация раскладывает всё по умолчаниям.
|
||||
for path, text in init_files(lay, sections, roadmap_sections,
|
||||
{VERSION_KEY: FORMAT_VERSION}).items():
|
||||
for path, text in init_files(lay, sections, roadmap_sections, {}).items():
|
||||
wr.file(path, text)
|
||||
backlog_lines = init_files(lay, sections, roadmap_sections, {})[lay.index("backlog")].splitlines()
|
||||
roadmap_lines = init_files(lay, sections, roadmap_sections, {})[lay.index("roadmap")].splitlines()
|
||||
|
||||
Reference in New Issue
Block a user