av-dev-pipeline: бриф удалён, проходы читают документы канона напрямую
- удалены скилл project-brief и контракт брифа; вместо них references/ project-facts.md — карта «что нужно проходу → где лежит» и таблица поразрядной деградации по документам - девять charter'ов, review-pipeline, task-pipeline и task-batch переписаны на пути канона; OpenSpec стал объявленной предпосылкой без ветки деградации - шаг синка документации переписан в построчный доклад, закрытие задачи — вызовом скилла av-dev-pm:tasks вместо строки-слота из CLAUDE.md - по находкам ревью: docs.py звал tasks.py из чужого каталога и выдавал его отказ окружения за дрейф; сверка миграций не видела рабочее дерево; плейсхолдер краснел вместо замечания; сверка capability проходила по совпадению с именем пакета; tasks.py не читал docs/.pm.json; скилл docs пересказывал канон в пяти местах
This commit is contained in:
@@ -17,7 +17,10 @@ description: Привести проект к канону документов
|
||||
пересказывается: два описания одной раскладки разъедутся, и работать будет то,
|
||||
которое прочитали последним. Прочитай его **до** первой правки.
|
||||
|
||||
Журнал версий — [references/changelog.md](references/changelog.md).
|
||||
- [references/skeletons.md](references/skeletons.md) — **что именно класть** в
|
||||
каждый незаполненный слот. Своей формой заглушку не выдумывай: `docs.py`
|
||||
узнаёт только плейсхолдер `<!-- заполнить: … -->` из шаблонов.
|
||||
- [references/changelog.md](references/changelog.md) — журнал версий канона.
|
||||
|
||||
## Три правила, из которых всё следует
|
||||
|
||||
@@ -53,9 +56,13 @@ python3 $ds version --dir <корень> # версия кано
|
||||
нельзя**. `check`, отчитавшийся «канон соблюдён» на проекте, где из шести файлов
|
||||
три лишние, хуже отсутствующего.
|
||||
|
||||
Машина проверяет пути, лишние файлы, битые ссылки, версию, нетронутые
|
||||
плейсхолдеры, маркеры долга и две сверки с кодом. **Ты** судишь о том, чего она
|
||||
не умеет:
|
||||
Машина **дрейфом** считает: отсутствующий путь канона, файл вне канона, битую
|
||||
ссылку, отставшую версию, capability без упоминания в обзоре, миграцию без правки
|
||||
`database.md`. **Замечанием** — незаполненный плейсхолдер и слабое упоминание
|
||||
capability: незаполненный канон это переходное состояние, а не отказ. Маркеры
|
||||
долга просто считает числом.
|
||||
|
||||
**Ты** судишь о том, чего она не умеет:
|
||||
|
||||
- **смысловой дубль** — `docs/specs/recognition.md` описывает то же, что
|
||||
capability `recognition`. Файлы разные, содержание одно;
|
||||
@@ -113,8 +120,8 @@ capability), `openspec/config.yaml`.
|
||||
Порядок важен — он минимизирует окно, в котором ссылки битые:
|
||||
|
||||
1. `docs/.pm.json` с `{"canon": <текущая версия>}` и путём миграций, если БД есть;
|
||||
2. каталоги канона и скелет: незаполненное — **одной честной информативной
|
||||
строкой**, а не «TBD» (см. canon.md, «Пустое называется пустым»);
|
||||
2. каталоги канона и скелет **по [references/skeletons.md](references/skeletons.md)**:
|
||||
незаполненное — одной честной информативной строкой, а не «TBD»;
|
||||
3. переносы содержимого;
|
||||
4. каталог задач — **вызови скилл `av-dev-pm:tasks`**, сценарий адаптации: он
|
||||
владеет форматом задач, включая переименование транслитных слагов в
|
||||
@@ -122,8 +129,15 @@ capability), `openspec/config.yaml`.
|
||||
5. починка ссылок на перенесённое во всём репозитории — `docs/`, `openspec/`,
|
||||
`CLAUDE.md`, `README.md`;
|
||||
6. удаление оригиналов — **только тех, чьё содержимое найдено в новом доме**;
|
||||
7. шаг `docs.py check` в гейт проекта;
|
||||
8. `docs.py check` — до зелёного в механизируемой части.
|
||||
7. **шаг `docs.py check` в гейт проекта.** Путь к скрипту — переменной с
|
||||
умолчанием на канонический путь маркетплейса, чтобы переустановка плагина не
|
||||
меняла `Taskfile`; шаг обязан **краснеть внятно**, если скрипт не найден, а не
|
||||
пропускаться. Передай ему базу диффа (`--base`) той же переменной, что и
|
||||
остальным шагам гейта: без неё сверка миграций со схемой не гоняется вовсе.
|
||||
Пример строки покажи человеку — гейт принадлежит проекту, и правит его он;
|
||||
8. `docs.py check` — до **отсутствия дрейфа**. Замечания (незаполненные
|
||||
плейсхолдеры, слабое упоминание capability) остаются: незаполненный канон это
|
||||
объявленное переходное состояние из шага 5, а не отказ.
|
||||
|
||||
### 5. Объяви переходное состояние
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
Плейсхолдер ставится только там, где ответ **обязан** быть и его не спросили.
|
||||
Всё, чего в проекте пока просто нет, описывается словами, а не плейсхолдером.
|
||||
|
||||
**Шаблоны — единственное место, где правило канона копируется намеренно.**
|
||||
`adr/README.md` и `review.md` уезжают в репозиторий проекта и обязаны там что-то
|
||||
говорить; определение при этом остаётся в [canon.md](canon.md). Отсюда
|
||||
обязанность: **правка такого правила в каноне тянет запись в
|
||||
[changelog.md](changelog.md)** с указанием, какой файл проекта поднимает
|
||||
`upgrade`. Без этого копия в проекте останется на старой версии молча.
|
||||
|
||||
## `docs/passport.md`
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -76,19 +76,25 @@ RETIRED = {
|
||||
|
||||
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
|
||||
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
|
||||
MD_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
|
||||
MD_LINK = re.compile(r"\[[^\]]*\]\(\s*<?([^)>\s]+)>?(?:\s+[\"'(][^)]*)?\)")
|
||||
FENCE = re.compile(r"^\s*(```|~~~)")
|
||||
|
||||
|
||||
INLINE_CODE = re.compile(r"`[^`\n]*`")
|
||||
|
||||
|
||||
def strip_code(text: str) -> str:
|
||||
"""Выкинуть блоки кода: пути в примерах и шаблонах — не ссылки, и краснеть
|
||||
на них значит краснеть на каждом образце документа."""
|
||||
"""Выкинуть блоки кода и вставки в обратных кавычках.
|
||||
|
||||
Путь в примере или в шаблоне — не ссылка, и краснеть на нём значит краснеть
|
||||
на каждом образце документа. Инлайн-код тоже: `[docs/backlog](docs/tasks/…)`
|
||||
в тексте про подписи ссылок — иллюстрация, а не ссылка."""
|
||||
out, inside = [], False
|
||||
for line in text.splitlines():
|
||||
if FENCE.match(line):
|
||||
inside = not inside
|
||||
continue
|
||||
out.append("" if inside else line)
|
||||
out.append("" if inside else INLINE_CODE.sub("", line))
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
@@ -226,7 +232,9 @@ def check_placeholders_and_debt(root: Path, rep: Report) -> None:
|
||||
text = strip_code(path.read_text(encoding="utf-8", errors="replace"))
|
||||
rel = path.relative_to(root)
|
||||
for what in PLACEHOLDER.findall(text):
|
||||
rep.error(f"{rel}: плейсхолдер шаблона не заполнен — {what}")
|
||||
# Замечание, а не дрейф: незаполненный канон — объявленное переходное
|
||||
# состояние, и краснеть на нём значит требовать выдумать содержание.
|
||||
rep.note(f"{rel}: плейсхолдер шаблона не заполнен — {what}")
|
||||
for what in DEBT_MARKER.findall(text):
|
||||
rep.debt(f"{rel}: {what}")
|
||||
|
||||
@@ -238,28 +246,60 @@ def check_capabilities(root: Path, rep: Report) -> None:
|
||||
rep.skip("openspec/specs/ нет — сверка capability с архитектурой неприменима")
|
||||
return
|
||||
if not arch.exists():
|
||||
rep.skip(
|
||||
"docs/architecture.md нет — capability не сверены с обзором "
|
||||
"(об отсутствии файла сказано отдельной строкой)"
|
||||
)
|
||||
return
|
||||
text = arch.read_text(encoding="utf-8", errors="replace")
|
||||
missing = [d.name for d in sorted(specs.iterdir()) if d.is_dir() and d.name not in text]
|
||||
for name in missing:
|
||||
rep.error(
|
||||
f"capability {name} есть в openspec/specs/, но не упомянута в "
|
||||
f"docs/architecture.md — обзор отстал от нормативных спек"
|
||||
)
|
||||
for d in sorted(specs.iterdir()):
|
||||
if not d.is_dir():
|
||||
continue
|
||||
name = d.name
|
||||
# Засчитываем только явное упоминание: ссылку на спеку или имя в обратных
|
||||
# кавычках. Голая подстрока совпадает с именем пакета или CLI-команды и
|
||||
# даёт ложное «упомянуто» — то есть проверку, проходящую не по той причине.
|
||||
explicit = f"openspec/specs/{name}" in text or f"`{name}`" in text
|
||||
loose = re.search(rf"\b{re.escape(name)}\b", text) is not None
|
||||
if explicit:
|
||||
continue
|
||||
if loose:
|
||||
rep.note(
|
||||
f"capability {name}: в docs/architecture.md есть слово «{name}», но "
|
||||
f"нет ни ссылки на openspec/specs/{name}, ни имени в обратных "
|
||||
f"кавычках — проверь, это про capability или про пакет"
|
||||
)
|
||||
else:
|
||||
rep.error(
|
||||
f"capability {name} есть в openspec/specs/, но не упомянута в "
|
||||
f"docs/architecture.md — обзор отстал от нормативных спек"
|
||||
)
|
||||
|
||||
|
||||
def changed_files(root: Path, base: str, rep: Report) -> list[str] | None:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "-C", str(root), "diff", "--name-only", f"{base}...HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||
rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})")
|
||||
return None
|
||||
return [line for line in out.stdout.splitlines() if line]
|
||||
"""Объединение закоммиченного, рабочего дерева и untracked.
|
||||
|
||||
Гейт гоняют ДО коммита, поэтому `base...HEAD` не видит ровно ту правку, ради
|
||||
которой проверка и заводилась: миграция уже лежит в дереве, но ещё не в
|
||||
истории. Пропущенная правка выглядела бы как зелёный шаг."""
|
||||
cmds = [
|
||||
["diff", "--name-only", base],
|
||||
["ls-files", "--others", "--exclude-standard"],
|
||||
]
|
||||
seen: list[str] = []
|
||||
for cmd in cmds:
|
||||
try:
|
||||
out = subprocess.run(
|
||||
["git", "-C", str(root), *cmd],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||||
rep.skip(f"сверка миграций пропущена: git не отдал дифф ({exc})")
|
||||
return None
|
||||
seen.extend(line for line in out.stdout.splitlines() if line)
|
||||
return sorted(set(seen))
|
||||
|
||||
|
||||
def check_migrations(root: Path, cfg: dict, base: str | None, rep: Report) -> None:
|
||||
@@ -292,17 +332,24 @@ def check_tasks(root: Path, rep: Report) -> None:
|
||||
if not script.exists():
|
||||
rep.skip(f"tasks.py не найден по пути {script} — согласованность задач не проверена")
|
||||
return
|
||||
# cwd=root обязателен: tasks.py отвергает --dir вне текущего каталога, и без
|
||||
# этого его отказ окружения (код 3) схлопнулся бы в наш дрейф (код 1).
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(script), "check", "--dir", str(tasks)],
|
||||
[sys.executable, str(script), "check", "--dir", "docs/tasks"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(root),
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
return
|
||||
if proc.returncode == 1:
|
||||
rep.error("tasks.py check нашёл дрейф в docs/tasks/ — разбирать его командой tasks.py")
|
||||
else:
|
||||
rep.error(f"tasks.py check отказал с кодом {proc.returncode}: {proc.stderr.strip()}")
|
||||
# Чужой код выхода не выдаём за свой: 3 это окружение, а не дрейф.
|
||||
rep.skip(
|
||||
f"tasks.py check не отработал (код {proc.returncode}): "
|
||||
f"{(proc.stderr or proc.stdout).strip().splitlines()[0] if (proc.stderr or proc.stdout).strip() else 'без сообщения'}"
|
||||
)
|
||||
|
||||
|
||||
# --- Отчёт ------------------------------------------------------------------
|
||||
|
||||
@@ -64,31 +64,24 @@ description: Вести содержимое документов канона
|
||||
**ADR цитирует решение оттуда и ссылается на источник.** Не пересказывает и не
|
||||
сочиняет заново.
|
||||
|
||||
Заводится, когда верно одно из трёх:
|
||||
**Триггер заведения, форма имени и правило замены — в
|
||||
[каноне](../canon/references/canon.md), раздел `adr/`.** Здесь они не
|
||||
повторяются: копия правила расходится с оригиналом на первой же смене версии
|
||||
канона, а расходится незаметно.
|
||||
|
||||
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
|
||||
- **намеренный отказ** от очевидного подхода — чтобы не переоткрывать «а почему
|
||||
мы не сделали X»;
|
||||
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
|
||||
`заменено на ADR-…`, а у новой в контексте строка «Заменяет ADR-…».
|
||||
Твоя часть — **применить триггер к этой задаче и сказать вслух, сработал он или
|
||||
нет**. Строка «adr/ — не требуется: решение рутинное» и есть то, ради чего
|
||||
чек-лист существует; её отсутствие неотличимо от «забыл посмотреть».
|
||||
|
||||
Не заводится для рутины и для того, что видно из кода и `git log`.
|
||||
|
||||
Порядок: имя `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение **принято**, слаг
|
||||
английский; тело по `docs/adr/template.md`; строка в индексе `docs/adr/README.md`
|
||||
сверху. Активная запись статуса не имеет.
|
||||
Порядок работы: открой архивный `design.md` change, найди в `Decisions` то, что
|
||||
проходит триггер, процитируй решение и его причину, сошлись на источник, добавь
|
||||
строку в индекс `docs/adr/README.md` сверху.
|
||||
|
||||
## Чистка `architecture.md`
|
||||
|
||||
Обзор не держит поведение — его нормативный дом `openspec/specs/`. Раздел, где
|
||||
поведение осталось, помечается маркером долга:
|
||||
|
||||
```
|
||||
<!-- канон: поведение → openspec/specs/<capability> -->
|
||||
```
|
||||
|
||||
`docs.py` считает маркеры и печатает числом; **гейт от них не краснеет** — это
|
||||
долг, а не отказ, иначе постепенный переезд стал бы невозможен.
|
||||
Обзор не держит поведение — его нормативный дом `openspec/specs/`. **Форма
|
||||
маркера долга и правило «гейт от них не краснеет» — в
|
||||
[каноне](../canon/references/canon.md), раздел `architecture.md`.**
|
||||
|
||||
Разбирается порциями: раздел вычищается той задачей, которая его касается.
|
||||
Содержимое не выбрасывается, а переезжает — требования в дельта-спеку change,
|
||||
@@ -97,45 +90,36 @@ description: Вести содержимое документов канона
|
||||
## Запись в `research/`
|
||||
|
||||
Наблюдение о внешнем мире: что реально шлёт источник, чем документация формата
|
||||
расходится с практикой. **Число — с провенансом**: команда или условия, которыми
|
||||
получено, чтобы его можно было перепроверить.
|
||||
расходится с практикой. **Требование провенанса и правило про расходящееся
|
||||
число — в [каноне](../canon/references/canon.md), раздел `research/`.**
|
||||
|
||||
Число без источника проход обязан читать как условие. Число, чей источник по
|
||||
ссылке не подтвердился, **не переписывается по догадке** — остаётся с пометкой
|
||||
«расходится с источником: там <что нашли>». Молча подставить «правильное» число
|
||||
хуже всего: расхождение перестанет быть видно, а причина останется.
|
||||
Твоя часть — заметить, что по ходу задачи узналось новое о внешних данных, и не
|
||||
дать этому остаться в контексте. Признак: ты правил разбор, опираясь на то, чего
|
||||
нет ни в одном документе.
|
||||
|
||||
## Запись в `review.md`
|
||||
|
||||
Два раздела с разными сроками жизни, и путать их нельзя.
|
||||
Файл держит два раздела с разными сроками жизни — журнал дефектов и настройку
|
||||
конвейера. **Что в каком и в какой форме — в
|
||||
[каноне](../canon/references/canon.md), раздел `review.md`**; подробности формы
|
||||
записи и выбор адреса, куда она ведёт, — в конвейере ревью,
|
||||
`references/review-journal.md`.
|
||||
|
||||
**Журнал дефектов.** Запись на каждый воспроизведённый дефект с пометкой
|
||||
**проскочил / пойман ревью**. Пишется сразу, а не ретроспективно: со временем
|
||||
теряется не факт, а причина непоймания — единственное, ради чего журнал есть.
|
||||
Форма: где, симптом, чем воспроизведён, почему не поймали (для проскочивших),
|
||||
что меняем. Вывод «ничего не меняем, цена поимки выше цены дефекта» — законный
|
||||
исход.
|
||||
|
||||
**Настройка конвейера.** Типовые узлы; типовые ложноположительные; вопросы к
|
||||
проходам поимённо с провенансом; недоступно проверке. Последний раздел делится
|
||||
на «не проверит ни один проход» (принципиальная граница, по факту промаха не
|
||||
пересматривается) и «перестали проверять сознательно» — этот **пересматривается
|
||||
первым**, как только что-то проскочило.
|
||||
Твоя часть на синке: **дефект пишется сразу**, а не «потом, когда починим».
|
||||
Со временем теряется не факт, а причина непоймания — единственное, ради чего
|
||||
журнал есть. И решение о сужении проверок (перестали звать проход, понизили
|
||||
профиль) обязано попасть в раздел настройки, а не остаться в отчёте ревью.
|
||||
|
||||
## Промоут в конвенции
|
||||
|
||||
Находка → конвенция → правило линтера → **удаление из прозы**. Процедура
|
||||
принадлежит конвейеру ревью и живёт в его `references/promote.md`; здесь только
|
||||
то, что касается документа:
|
||||
Находка → конвенция → правило линтера → **удаление из прозы**. Процедура целиком
|
||||
принадлежит конвейеру ревью и живёт в его `references/promote.md`; роль каталога
|
||||
конвенций — в [каноне](../canon/references/canon.md).
|
||||
|
||||
- формулировка — **проверяемое свойство**, а не совет;
|
||||
- в прозе остаётся только то, что принципиально не выражается правилом;
|
||||
- как только правило работает, формулировка из `conventions/<тема>.md`
|
||||
**удаляется**, а правило попадает в перечень механизированного в
|
||||
`conventions/README.md` со ссылкой на место механизации.
|
||||
|
||||
Непойманное место механизации означает, что проход по конвенциям будет
|
||||
добросовестно проверять уже проверенное.
|
||||
Твоя часть — **третий шаг, который пропускают чаще всего**: правило заработало,
|
||||
а формулировка осталась в прозе, и проход продолжает проверять уже проверенное.
|
||||
На синке это отдельная строка: «conventions/ — правило X механизировано,
|
||||
формулировка удалена» либо «не требуется».
|
||||
|
||||
## Чего этот скилл не делает
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ description: Завести новый проект — сессия вопро
|
||||
которого дальше работают все остальные скиллы.
|
||||
|
||||
**Определение канона — [канон](../canon/references/canon.md).** Читается до
|
||||
первого вопроса: интервью идёт по слотам канона, а не по вкусу.
|
||||
первого вопроса: интервью идёт по слотам канона, а не по вкусу. Что класть в
|
||||
каждый файл — [скелеты](../canon/references/skeletons.md); своей формы заглушки
|
||||
не выдумывай, `docs.py` узнаёт только плейсхолдер оттуда.
|
||||
|
||||
## Что `init` физически не может произвести
|
||||
|
||||
@@ -69,10 +71,12 @@ description: Завести новый проект — сессия вопро
|
||||
4. Напиши заполняемые документы. **Бриф переезжает в `passport.md`** и
|
||||
отдельным файлом не остаётся: два дома для одного замысла разойдутся на
|
||||
первом же уточнении.
|
||||
5. Заведи скелет остальных — каждый с честной строкой.
|
||||
5. Заведи скелет остальных по [скелетам](../canon/references/skeletons.md) —
|
||||
каждый с честной строкой.
|
||||
6. Каталог задач и первые цели — **вызови скилл `av-dev-pm:tasks`**: он владеет
|
||||
форматом целей и задач.
|
||||
7. `docs.py check` из скилла `canon` — до зелёного в механизируемой части.
|
||||
7. `docs.py check` из скилла `canon` — до отсутствия дрейфа. Замечания о
|
||||
незаполненных плейсхолдерах остаются: их закрывает не `init`, а работа.
|
||||
8. Покажи человеку, что получилось, и **отдельным списком** — что выведено из
|
||||
брифа, что предположено, что осталось неизвестным. Правят по этим строкам.
|
||||
|
||||
|
||||
@@ -83,7 +83,8 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_NAME = ".tasks.json"
|
||||
CONFIG_NAME = ".tasks.json" # прежний дом настроек, читается для совместимости
|
||||
PM_CONFIG_REL = "../.pm.json" # текущий дом: docs/.pm.json, ключ "tasks"
|
||||
|
||||
EXIT_OK = 0
|
||||
EXIT_DRIFT = 1
|
||||
@@ -278,15 +279,42 @@ class Layout:
|
||||
|
||||
|
||||
def load_config(root: Path) -> dict:
|
||||
"""Настройки каталога задач.
|
||||
|
||||
Дом один — `docs/.pm.json`, ключ `tasks`: один конфиг на весь канон, а не по
|
||||
одному на каталог. Прежний `<tasks>/.tasks.json` читается, пока живы проекты,
|
||||
которые ещё не переехали; когда есть оба, побеждает `.pm.json`, и об этом
|
||||
говорится вслух, потому что молча выбранный из двух конфиг — это дрейф,
|
||||
который потом никто не объяснит.
|
||||
"""
|
||||
pm = (root / PM_CONFIG_REL).resolve()
|
||||
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)
|
||||
return _validate_config(section, pm)
|
||||
path = root / CONFIG_NAME
|
||||
if not path.is_file():
|
||||
return {}
|
||||
return _validate_config(_read_json(path), path)
|
||||
|
||||
|
||||
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}")
|
||||
if not isinstance(data, dict):
|
||||
raise Env(f"{path}: ожидался объект с настройками")
|
||||
return data
|
||||
|
||||
|
||||
def _validate_config(data: dict, path: Path) -> dict:
|
||||
unknown = set(data) - set(DEFAULTS)
|
||||
if unknown:
|
||||
raise Env(f"{path}: неизвестные ключи: {', '.join(sorted(unknown))}"
|
||||
@@ -351,10 +379,10 @@ def resolve_layout(explicit: str | None) -> Layout:
|
||||
return Layout(rel if str(rel) != "." else candidate, load_config(candidate))
|
||||
if (base / ".git").exists():
|
||||
break # выше корня репозитория не ищем
|
||||
raise Env("каталог задач не найден: ни --dir, ни .tasks.json вверх от"
|
||||
f" {here}, ни умолчания (docs/tasks, tasks, doc/tasks)."
|
||||
" Путь каталога называет CLAUDE.md проекта; новый проект —"
|
||||
" tasks.py init --dir <путь>")
|
||||
raise Env("каталог задач не найден: ни --dir, ни docs/tasks вверх от"
|
||||
f" {here}. По канону путь всегда docs/tasks; чужую раскладку"
|
||||
" переводит скилл av-dev-pm:canon, новый проект —"
|
||||
" tasks.py init --dir docs/tasks")
|
||||
|
||||
|
||||
# --- Чтение индексов ---
|
||||
|
||||
Reference in New Issue
Block a user