форма config.yaml сверяется с живым openspec, а не с памятью
Проверка формы знала имя схемы и перечень артефактов константами — и это не наше решение, а состояние чужого инструмента. OpenSpec переименует артефакт: правила под прежним именем перестанут применяться, конфиг останется выглядеть написанным, канон продолжит требовать прежнее. Молчат при этом все три стороны, и заметить расхождение было некому. Сторожем поставлено сравнение версий. check спрашивает openspec --version — десятые доли секунды — и сравнивает major.minor с той, на которой форма сверялась. Разошлось — замечание, не отказ, с именем команды, которая перепроверяет. Патч-версия в сравнение не берётся намеренно: формы она не меняет, а нагоняй на каждый багфикс приучает пролистывать весь блок. Перепроверяет docs.py openspec-form: берёт openspec templates --json, то есть перечень артефактов текущей схемы, и печатает, что разошлось с константами. Дорогой вызов вынесен из check сознательно — он стоит втрое дороже опроса версии, а ответ меняется только вместе с версией. Дешёвая проверка служит воротами дорогой, и дорогая не ржавеет, потому что зовут её не по памяти. Чинится расхождение в плагине, а не в проекте, и команда печатает три адреса правки списком: константы скрипта, скелет, журнал версий канона. Пятой проверкой формы стали ключи под rules: — это имена артефактов, и правило, адресованное несуществующему, не применяется молча. rules.spec вместо rules.specs даёт конфиг, выглядящий написанным и не работающий. Первый вариант этой проверки искал ключи отступом по всему файлу и нашёл их внутри литерального блока context: строки «Language: Russian» и «av-dev-pm:review-pipeline» выглядят ключами. Оба живых проекта из-за этого покраснели на правде. Теперь разбор идёт от строки rules: до следующего ключа нулевой колонки; на тех же проектах чисто, а опечатка в имени артефакта по-прежнему находится. Решение — 48. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3083,3 +3083,47 @@ JJJ): у профиля обязан быть один правильный от
|
|||||||
вторую.** Файл на месте, валиден, с правильным именем — и пуст по существу:
|
вторую.** Файл на месте, валиден, с правильным именем — и пуст по существу:
|
||||||
это худший вид пробела, потому что выглядит он как его отсутствие.
|
это худший вид пробела, потому что выглядит он как его отсутствие.
|
||||||
|
|
||||||
|
|
||||||
|
## 48. Форма чужого инструмента держится опросом инструмента, а не памятью (2026-08-07)
|
||||||
|
|
||||||
|
**АЕАИГ. Схема и артефакты OpenSpec записаны в скрипте как слепок, и у слепка
|
||||||
|
есть сторож.** Проверка формы `config.yaml` знает имя схемы и перечень артефактов
|
||||||
|
(`proposal`, `specs`, `design`, `tasks`). Это не наше решение, а состояние чужого
|
||||||
|
инструмента: OpenSpec переименует артефакт — правила под прежним именем перестанут
|
||||||
|
применяться, конфиг останется выглядеть написанным, а канон продолжит требовать
|
||||||
|
прежнее. Все три стороны при этом молчат.
|
||||||
|
|
||||||
|
Сторожем сделано сравнение версий: `check` спрашивает `openspec --version`
|
||||||
|
(десятые доли секунды) и сравнивает `major.minor` с той, на которой форма
|
||||||
|
сверялась. Разошлось — **замечание**, не отказ, с именем команды, которая
|
||||||
|
перепроверяет. Патч-версия в сравнение не берётся намеренно: формы она не
|
||||||
|
меняет, а нагоняй на каждый багфикс приучает пролистывать весь блок.
|
||||||
|
|
||||||
|
**АЕАИД. Перепроверка спрашивает инструмент, а не нас.** `docs.py openspec-form`
|
||||||
|
берёт `openspec templates --json` — перечень артефактов текущей схемы — и
|
||||||
|
печатает, что разошлось с константами. Дорогой вызов вынесен из `check`
|
||||||
|
сознательно: он стоит втрое дороже опроса версии, а ответ меняется только вместе
|
||||||
|
с версией. Дешёвая проверка служит **воротами** дорогой, и дорогая не ржавеет,
|
||||||
|
потому что зовут её не по памяти.
|
||||||
|
|
||||||
|
**Чинится расхождение в плагине, а не в проекте, и это сказано в трёх местах.**
|
||||||
|
Проект в такой ситуации не виноват и починить ничего не может: у него нет ни
|
||||||
|
констант скрипта, ни скелета, ни журнала версий канона. Замечание поэтому
|
||||||
|
адресовано владельцу плагина, а команда печатает три адреса правки списком.
|
||||||
|
|
||||||
|
**Заодно поймано ложное срабатывание на живом конфиге.** Первый вариант искал
|
||||||
|
ключи `rules` отступом по всему файлу и нашёл их внутри литерального блока
|
||||||
|
`context: |`: строки «Language: Russian» и «av-dev-pm:review-pipeline» выглядят
|
||||||
|
ключами. Проверка теперь идёт от строки `rules:` до следующего ключа нулевой
|
||||||
|
колонки. Правило, краснеющее на правде, хуже отсутствующего — его перестают
|
||||||
|
читать целиком.
|
||||||
|
|
||||||
|
### Что из этого следует
|
||||||
|
|
||||||
|
169. **Знание о чужом инструменте, записанное у себя, — это слепок с датой.**
|
||||||
|
Он законен, пока рядом стоит тот, кто заметит, что дата протухла; без
|
||||||
|
сторожа он превращается в уверенное враньё.
|
||||||
|
170. **Дешёвая проверка как ворота дорогой.** Опрос версии стоит копейки и
|
||||||
|
точно говорит, могла ли измениться дорогая величина. Так дорогая проверка
|
||||||
|
остаётся редкой и при этом не забытой.
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,19 @@ ds="$CLAUDE_PLUGIN_ROOT/skills/canon/scripts/docs.py"
|
|||||||
|
|
||||||
python3 $ds check --dir <корень> [--base <rev>] # раскладка, ссылки, версия, сверки
|
python3 $ds check --dir <корень> [--base <rev>] # раскладка, ссылки, версия, сверки
|
||||||
python3 $ds version --dir <корень> # версия канона скрипта и проекта
|
python3 $ds version --dir <корень> # версия канона скрипта и проекта
|
||||||
|
python3 $ds openspec-form # форма config.yaml против живого OpenSpec
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**`openspec-form` зовут не на каждом прогоне, а когда о нём попросил `check`.**
|
||||||
|
Форма `openspec/config.yaml` описана в каноне слепком чужого инструмента — имя
|
||||||
|
схемы и перечень артефактов, — и слепок стареет молча: OpenSpec переименует
|
||||||
|
артефакт, правила под старым именем перестанут применяться, а конфиг останется
|
||||||
|
выглядеть написанным. Поэтому `check` каждым прогоном сравнивает `major.minor`
|
||||||
|
установленного OpenSpec с тем, на котором форма сверялась, и при расхождении
|
||||||
|
даёт замечание с этой командой. Команда ничего не правит: она спрашивает
|
||||||
|
инструмент и печатает, что разошлось. **Чинится это в плагине, а не в проекте** —
|
||||||
|
константы `docs.py`, скелет в `skeletons.md` и запись в журнал версий канона.
|
||||||
|
|
||||||
**Коды выхода — тот же словарь, что у `tasks.py`:** 0 сошлось, 1 дрейф, 2 ошибка
|
**Коды выхода — тот же словарь, что у `tasks.py`:** 0 сошлось, 1 дрейф, 2 ошибка
|
||||||
употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте.
|
употребления, 3 окружение, 4 внутренний сбой. Ветвись на коде, а не на тексте.
|
||||||
|
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ kebab-case.** Причина не эстетическая: имя файла с
|
|||||||
знания, где лежит граница домена. Это ровно тот класс, против которого написан
|
знания, где лежит граница домена. Это ровно тот класс, против которого написан
|
||||||
весь канон, и потому здесь он проверяется машиной, а не чтением.
|
весь канон, и потому здесь он проверяется машиной, а не чтением.
|
||||||
|
|
||||||
Проверяется четыре вещи, и каждая — про молчащий пробел, а не про вкус:
|
Проверяется пять вещей, и каждая — про молчащий пробел, а не про вкус:
|
||||||
|
|
||||||
1. **`openspec/` есть.** Нет — нет и дома темы `requirements`.
|
1. **`openspec/` есть.** Нет — нет и дома темы `requirements`.
|
||||||
2. **Имя файла `config.yaml`.** `config.yml` OpenSpec не читает и об этом не
|
2. **Имя файла `config.yaml`.** `config.yml` OpenSpec не читает и об этом не
|
||||||
@@ -439,8 +439,22 @@ kebab-case.** Причина не эстетическая: имя файла с
|
|||||||
4. **`context` называет `passport` и `CLAUDE.md`.** Предложение пишется **до**
|
4. **`context` называет `passport` и `CLAUDE.md`.** Предложение пишется **до**
|
||||||
того, как кто-либо откроет `docs/`; без этих двух адресов его пишут, не зная
|
того, как кто-либо откроет `docs/`; без этих двух адресов его пишут, не зная
|
||||||
ни границы домена, ни инвариантов.
|
ни границы домена, ни инвариантов.
|
||||||
|
5. **Ключи под `rules:` — имена артефактов схемы** (`proposal`, `specs`,
|
||||||
|
`design`, `tasks`). Правило, адресованное несуществующему артефакту, не
|
||||||
|
применяется и об этом молчит: `rules.spec` вместо `rules.specs` — конфиг,
|
||||||
|
выглядящий написанным и не работающий.
|
||||||
|
|
||||||
Пятого — «нет ли здесь пересказа» — машина не проверяет: отличить ссылку от
|
**Схема и перечень артефактов — слепок чужого инструмента, и он стареет.**
|
||||||
|
OpenSpec переименует артефакт или сменит схему — правила под прежним именем
|
||||||
|
перестанут действовать молча, а канон будет продолжать требовать прежнее.
|
||||||
|
Поэтому за свежестью слепка следит машина: `check` сравнивает `major.minor`
|
||||||
|
установленного OpenSpec с версией, на которой форма сверялась, и при расхождении
|
||||||
|
даёт **замечание** (не отказ: патч-версии формы не меняют, а нагоняй на каждый
|
||||||
|
багфикс приучает пролистывать блок). Перепроверяет `docs.py openspec-form` — он
|
||||||
|
спрашивает сам инструмент и печатает, что разошлось. **Чинится это в плагине, а
|
||||||
|
не в проекте:** константы скрипта, скелет и запись в журнал версий канона.
|
||||||
|
|
||||||
|
Шестого — «нет ли здесь пересказа» — машина не проверяет: отличить ссылку от
|
||||||
пересказа она не умеет. Это работа `doc-consistency`, и раздел «Что проверяет
|
пересказа она не умеет. Это работа `doc-consistency`, и раздел «Что проверяет
|
||||||
машина, а что человек» называет её строкой.
|
машина, а что человек» называет её строкой.
|
||||||
|
|
||||||
@@ -513,7 +527,8 @@ kebab-case.** Причина не эстетическая: имя файла с
|
|||||||
| маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` |
|
| маркеры долга — числом | **протухший факт, разошедшийся с кодом** | `doc-code-drift` |
|
||||||
| миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` |
|
| миграция изменена, а `database.md` нет | зависимость в манифесте, не названная в обзоре | `doc-code-drift` |
|
||||||
| capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` |
|
| capability без упоминания в `architecture.md` | второй способ там, где обзор обещал единственный | `doc-code-drift` |
|
||||||
| `openspec/config.yaml`: имя, `schema`, незаменённый пример, адреса паспорта и `CLAUDE.md` | **пересказ документа канона в `context` вместо ссылки** | `doc-consistency` |
|
| `openspec/config.yaml`: имя, `schema`, незаменённый пример, адреса паспорта и `CLAUDE.md`, ключи `rules` против артефактов схемы | **пересказ документа канона в `context` вместо ссылки** | `doc-consistency` |
|
||||||
|
| версия OpenSpec разошлась с той, на которой сверена форма `config.yaml` | придирки валидатора: сменились ли они | никакой — проявляются отказом `openspec validate --strict` |
|
||||||
| | связность и читаемость | `doc-wording` |
|
| | связность и читаемость | `doc-wording` |
|
||||||
|
|
||||||
**Агентов двое, и разведены они по глубине, а не по охвату.** `doc-consistency`
|
**Агентов двое, и разведены они по глубине, а не по охвату.** `doc-consistency`
|
||||||
|
|||||||
@@ -38,11 +38,17 @@ capability по имени пакета и без единого `SHALL`.
|
|||||||
артефакта**: язык, правила именования capability, придирки валидатора и
|
артефакта**: язык, правила именования capability, придирки валидатора и
|
||||||
**адреса** документов канона. Пересказ паспорта, инвариантов, конвенций и
|
**адреса** документов канона. Пересказ паспорта, инвариантов, конвенций и
|
||||||
правил ревью в него не переносится.
|
правил ревью в него не переносится.
|
||||||
3. **`docs.py check` проверяет четыре вещи:** каталог `openspec/` есть; файл
|
3. **`docs.py check` проверяет пять вещей:** каталог `openspec/` есть; файл
|
||||||
называется `config.yaml` (`config.yml` OpenSpec читать не станет и об этом не
|
называется `config.yaml` (`config.yml` OpenSpec читать не станет и об этом не
|
||||||
сообщит); `context` и `rules.specs` не остались примером, а правила для
|
сообщит); `context` и `rules.specs` не остались примером, а правила для
|
||||||
`specs` называют `SHALL`; `context` называет `passport` и `CLAUDE.md`.
|
`specs` называют `SHALL`; `context` называет `passport` и `CLAUDE.md`; ключи
|
||||||
4. **Пятое проверяет агент.** Отличить ссылку на документ от пересказа документа
|
под `rules:` — имена артефактов схемы, а не опечатки.
|
||||||
|
4. **За свежестью формы следит машина, а не память.** Схема и перечень
|
||||||
|
артефактов — слепок чужого инструмента; `check` сравнивает `major.minor`
|
||||||
|
установленного OpenSpec с версией, на которой форма сверялась, и при
|
||||||
|
расхождении даёт замечание. Перепроверяет `docs.py openspec-form`, и чинится
|
||||||
|
расхождение **в плагине, а не в проекте**.
|
||||||
|
5. **Шестое проверяет агент.** Отличить ссылку на документ от пересказа документа
|
||||||
машина не умеет — это работа `doc-consistency`, и в таблице «Что проверяет
|
машина не умеет — это работа `doc-consistency`, и в таблице «Что проверяет
|
||||||
машина, а что человек» она стоит строкой.
|
машина, а что человек» она стоит строкой.
|
||||||
|
|
||||||
|
|||||||
@@ -490,6 +490,13 @@ rules:
|
|||||||
дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и
|
дополняет своим (стек, разведка, особенности домена), но **адреса паспорта и
|
||||||
`CLAUDE.md` обязательны** — их отсутствие `docs.py check` называет отказом.
|
`CLAUDE.md` обязательны** — их отсутствие `docs.py check` называет отказом.
|
||||||
|
|
||||||
|
**Ключи под `rules:` — имена артефактов схемы**, а не свободные слова:
|
||||||
|
`proposal`, `specs`, `design`, `tasks`. Правило под чужим именем не применяется
|
||||||
|
и об этом не сообщает, поэтому `rules.spec` вместо `rules.specs` даёт конфиг,
|
||||||
|
выглядящий написанным и не работающий; `docs.py check` такой ключ называет.
|
||||||
|
Перечень артефактов задаёт OpenSpec, а не канон, — за его актуальностью следит
|
||||||
|
`docs.py openspec-form`.
|
||||||
|
|
||||||
## `docs/.pm.json`
|
## `docs/.pm.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -89,6 +89,27 @@ OPENSPEC_POINTERS = [
|
|||||||
("CLAUDE.md", "инварианты и семантика гейта останутся непрочитанными"),
|
("CLAUDE.md", "инварианты и семантика гейта останутся непрочитанными"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# --- Форма config.yaml сверена с живым OpenSpec ------------------------------
|
||||||
|
#
|
||||||
|
# Три константы ниже — **слепок чужого инструмента**, а не наше решение. Схема,
|
||||||
|
# перечень артефактов и версия, на которой это проверено, живут в OpenSpec и
|
||||||
|
# меняются без нашего участия; здесь они записаны, чтобы проверка шла без запуска
|
||||||
|
# node на каждом прогоне.
|
||||||
|
#
|
||||||
|
# Слепок стареет, и потому есть кто, кто это замечает: `check` сравнивает
|
||||||
|
# major.minor установленного OpenSpec с OPENSPEC_CHECKED и, если они разошлись,
|
||||||
|
# говорит замечанием «форма не перепроверена». Перепроверяет `docs.py
|
||||||
|
# openspec-form` — он спрашивает сам инструмент и печатает, что разошлось.
|
||||||
|
# Патч-версия сравнением намеренно не берётся: форма конфига в ней не меняется, а
|
||||||
|
# замечание на каждый багфикс приучило бы пролистывать весь блок.
|
||||||
|
OPENSPEC_CHECKED = "1.5"
|
||||||
|
OPENSPEC_SCHEMA = "spec-driven"
|
||||||
|
|
||||||
|
# Артефакты схемы. Ключ `rules:` адресуется артефакту, и адресованный
|
||||||
|
# несуществующему **молча не действует** — ровно тот класс, ради которого вся
|
||||||
|
# проверка и заведена.
|
||||||
|
OPENSPEC_ARTIFACTS = ("proposal", "specs", "design", "tasks")
|
||||||
|
|
||||||
# Служебное в docs/ и каталог, который ведёт tasks.py. Оба процессные, но
|
# Служебное в docs/ и каталог, который ведёт tasks.py. Оба процессные, но
|
||||||
# проверок формы у них нет: .pm.json не markdown, tasks/ ведёт другой скрипт.
|
# проверок формы у них нет: .pm.json не markdown, tasks/ ведёт другой скрипт.
|
||||||
NOT_DOCS = {".pm.json", "tasks"}
|
NOT_DOCS = {".pm.json", "tasks"}
|
||||||
@@ -468,6 +489,31 @@ def doc_text(root: Path, name: str) -> str | None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rules_keys(live: str) -> list[str]:
|
||||||
|
"""Имена артефактов, которым адресованы правила, — и только они.
|
||||||
|
|
||||||
|
Идём от строки `rules:` до следующего ключа нулевой колонки, а не ищем
|
||||||
|
отступ по всему файлу: блок `context: |` — литеральный скаляр, внутри него
|
||||||
|
строки вида «Language: Russian» и «av-dev-pm:review-pipeline» выглядят
|
||||||
|
ключами и дали бы находку на ровном месте. Проверено на живом конфиге,
|
||||||
|
который так и падал.
|
||||||
|
"""
|
||||||
|
out: list[str] = []
|
||||||
|
inside = False
|
||||||
|
for line in live.splitlines():
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
if not line[0].isspace():
|
||||||
|
inside = line.startswith("rules:")
|
||||||
|
continue
|
||||||
|
if not inside:
|
||||||
|
continue
|
||||||
|
m = re.fullmatch(r" ([A-Za-z_-]+):\s*", line)
|
||||||
|
if m:
|
||||||
|
out.append(m.group(1))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def check_openspec(root: Path, rep: Report) -> None:
|
def check_openspec(root: Path, rep: Report) -> None:
|
||||||
"""Настройка OpenSpec заведена и не осталась примером из коробки.
|
"""Настройка OpenSpec заведена и не осталась примером из коробки.
|
||||||
|
|
||||||
@@ -507,11 +553,13 @@ def check_openspec(root: Path, rep: Report) -> None:
|
|||||||
|
|
||||||
schema = re.search(r"(?m)^schema:\s*(\S+)", live)
|
schema = re.search(r"(?m)^schema:\s*(\S+)", live)
|
||||||
if schema is None:
|
if schema is None:
|
||||||
rep.error("в openspec/config.yaml нет ключа schema — ожидается spec-driven")
|
rep.error(
|
||||||
elif schema.group(1) != "spec-driven":
|
f"в openspec/config.yaml нет ключа schema — ожидается {OPENSPEC_SCHEMA}"
|
||||||
|
)
|
||||||
|
elif schema.group(1) != OPENSPEC_SCHEMA:
|
||||||
rep.error(
|
rep.error(
|
||||||
f"schema в openspec/config.yaml — {schema.group(1)}, а канон описан "
|
f"schema в openspec/config.yaml — {schema.group(1)}, а канон описан "
|
||||||
f"для spec-driven"
|
f"для {OPENSPEC_SCHEMA}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if "context" not in keys:
|
if "context" not in keys:
|
||||||
@@ -539,6 +587,54 @@ def check_openspec(root: Path, rep: Report) -> None:
|
|||||||
"проекта об этом молчит"
|
"проекта об этом молчит"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Ключ под rules: — имя артефакта схемы. Опечатка или устаревшее имя не
|
||||||
|
# ломает ничего видимого: правила просто не применяются, а конфиг выглядит
|
||||||
|
# написанным.
|
||||||
|
for name in rules_keys(live):
|
||||||
|
if name not in OPENSPEC_ARTIFACTS:
|
||||||
|
rep.error(
|
||||||
|
f"rules.{name} в openspec/config.yaml — такого артефакта у схемы "
|
||||||
|
f"{OPENSPEC_SCHEMA} нет ({', '.join(OPENSPEC_ARTIFACTS)}): правила "
|
||||||
|
f"под ним не применяются и молчат об этом"
|
||||||
|
)
|
||||||
|
|
||||||
|
check_openspec_fresh(rep)
|
||||||
|
|
||||||
|
|
||||||
|
def openspec_cli(args: list[str]) -> str | None:
|
||||||
|
"""Спросить сам инструмент. None — его нет или он не ответил."""
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
["openspec", *args], capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
return out.stdout.strip() if out.returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def check_openspec_fresh(rep: Report) -> None:
|
||||||
|
"""Не устарел ли наш слепок формы config.yaml.
|
||||||
|
|
||||||
|
Стоит один запуск `openspec --version` — десятые доли секунды. Перечень
|
||||||
|
артефактов и имя схемы отсюда не спрашиваются намеренно: они стоят втрое
|
||||||
|
дороже, а меняются только вместе с версией, и потому за ними ходит отдельная
|
||||||
|
команда `openspec-form`, а эта проверка говорит, когда её звать.
|
||||||
|
"""
|
||||||
|
got = openspec_cli(["--version"])
|
||||||
|
if got is None:
|
||||||
|
rep.skip(
|
||||||
|
"openspec не отвечает (нет на PATH?) — актуальность формы "
|
||||||
|
"config.yaml не проверялась"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
installed = ".".join(got.split(".")[:2])
|
||||||
|
if installed != OPENSPEC_CHECKED:
|
||||||
|
rep.note(
|
||||||
|
f"форма openspec/config.yaml сверена с OpenSpec {OPENSPEC_CHECKED}, "
|
||||||
|
f"установлен {got}: перепроверить — `docs.py openspec-form`. Пока не "
|
||||||
|
f"перепроверено, проверки формы судят по прежней схеме"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_capabilities(root: Path, rep: Report) -> None:
|
def check_capabilities(root: Path, rep: Report) -> None:
|
||||||
specs = root / "openspec" / "specs"
|
specs = root / "openspec" / "specs"
|
||||||
@@ -718,6 +814,71 @@ def cmd_version(args: argparse.Namespace) -> int:
|
|||||||
return OK
|
return OK
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_openspec_form(args: argparse.Namespace) -> int:
|
||||||
|
"""Перепроверить слепок формы config.yaml по живому OpenSpec.
|
||||||
|
|
||||||
|
Ничего не правит и не трогает проект: спрашивает инструмент и печатает, что
|
||||||
|
разошлось с константами скрипта. Чинит человек — правкой констант, скелета в
|
||||||
|
skeletons.md и записью в журнал версий канона, если форма действительно
|
||||||
|
поменялась.
|
||||||
|
"""
|
||||||
|
version = openspec_cli(["--version"])
|
||||||
|
if version is None:
|
||||||
|
fail(
|
||||||
|
ENV,
|
||||||
|
"openspec не отвечает: поставь его или проверь PATH — "
|
||||||
|
"перепроверять форму нечем",
|
||||||
|
)
|
||||||
|
raw = openspec_cli(["templates", "--json"])
|
||||||
|
if raw is None:
|
||||||
|
fail(ENV, "`openspec templates --json` не отработал — схему не спросить")
|
||||||
|
try:
|
||||||
|
artifacts = tuple(json.loads(raw))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
fail(ENV, f"`openspec templates --json` отдал неразбираемое: {exc}")
|
||||||
|
|
||||||
|
print(f"OpenSpec установлен: {version}")
|
||||||
|
print(f"форма сверена с: {OPENSPEC_CHECKED}")
|
||||||
|
print(f"артефакты схемы: {', '.join(artifacts)}")
|
||||||
|
print(f"записано в скрипте: {', '.join(OPENSPEC_ARTIFACTS)}")
|
||||||
|
|
||||||
|
diffs: list[str] = []
|
||||||
|
if ".".join(version.split(".")[:2]) != OPENSPEC_CHECKED:
|
||||||
|
diffs.append(
|
||||||
|
f"версия: поднять OPENSPEC_CHECKED до "
|
||||||
|
f"{'.'.join(version.split('.')[:2])} — но только после того, как "
|
||||||
|
f"остальные строки этого отчёта сойдутся"
|
||||||
|
)
|
||||||
|
for name in artifacts:
|
||||||
|
if name not in OPENSPEC_ARTIFACTS:
|
||||||
|
diffs.append(
|
||||||
|
f"новый артефакт {name}: решить, нужны ли ему правила в rules, "
|
||||||
|
f"и добавить имя в OPENSPEC_ARTIFACTS"
|
||||||
|
)
|
||||||
|
for name in OPENSPEC_ARTIFACTS:
|
||||||
|
if name not in artifacts:
|
||||||
|
diffs.append(
|
||||||
|
f"артефакта {name} у схемы больше нет: правила под ним в конфигах "
|
||||||
|
f"проектов молчат — убрать из OPENSPEC_ARTIFACTS, из скелета и "
|
||||||
|
f"записать в журнал версий канона"
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
if not diffs:
|
||||||
|
print("Слепок сходится. Осталось глазами: не изменились ли придирки")
|
||||||
|
print("валидатора — их скрипт проверить не может, они проявляются только")
|
||||||
|
print("отказом `openspec validate --strict` на живой спеке.")
|
||||||
|
return OK
|
||||||
|
print("Разошлось:")
|
||||||
|
for line in diffs:
|
||||||
|
print(f" - {line}")
|
||||||
|
print()
|
||||||
|
print("Правится в трёх местах сразу: константы этого скрипта, скелет")
|
||||||
|
print("`openspec/config.yaml` в skeletons.md и запись в changelog.md —")
|
||||||
|
print("иначе проекты останутся на прежней форме молча.")
|
||||||
|
return DRIFT
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="docs.py",
|
prog="docs.py",
|
||||||
@@ -734,6 +895,12 @@ def main() -> int:
|
|||||||
p_ver.add_argument("--dir", default=".", help="корень проекта")
|
p_ver.add_argument("--dir", default=".", help="корень проекта")
|
||||||
p_ver.set_defaults(func=cmd_version)
|
p_ver.set_defaults(func=cmd_version)
|
||||||
|
|
||||||
|
p_form = sub.add_parser(
|
||||||
|
"openspec-form",
|
||||||
|
help="перепроверить форму config.yaml по живому OpenSpec",
|
||||||
|
)
|
||||||
|
p_form.set_defaults(func=cmd_openspec_form)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
return args.func(args)
|
return args.func(args)
|
||||||
|
|||||||
Reference in New Issue
Block a user