канон 4: слаг подкреплён проверкой, обещанный судья заведён
Оба пункта заметок оказались одним классом: правило записано и никем не исполняется. Слаги. canon.md говорил «слаги файлов, capability и задач — английские, kebab-case» одной строкой в хвосте раскладки, а docs.py имён файлов не смотрел вовсе. Итог нашёлся в самом плагине: единственный пример ADR в скилле docs назывался ADR-2026-08-03-ochered-tablicej. Раскладка канона при этом приглашала к нарушению — в схеме стояли плейсхолдеры <тема>.md, то есть слово «тема» по-русски там, где надо писать <slug>. docs.py check теперь смотрит имена: кириллица и не-kebab-case жёстко, форма ADR-ГГГГ-ММ-ДД-slug.md жёстко, транслит эвристикой, то есть замечанием. Проверяются docs/conventions, docs/research, docs/adr и имена capability; каталог задач не трогается — его слаги ведёт tasks.py. Набор маркеров транслита подобран так, чтобы ложных срабатываний не было вовсе: выброшены ost (ловит post, cost), sch (schema), ya (yaml), nost (nostalgia), хвост ii (radii). Цена названа в комментарии — sostoyanie-partii проходит мимо. Правило, краснеющее на правде, приучает пролистывать весь блок, и это дороже пропуска. Агенты. В canon.md есть таблица «Что проверяет машина, а что человек», и её правая колонка — смысловой дубль, поведение в architecture.md, протухший факт, достаточность честной строки — три версии описывала работу, которую никто не делал: скилл canon предлагал агенту судить об этом самому, то есть проверять то, что он же и писал. Заведены двое, разрез по глубине — тот же довод, что развёл task-form и doc-wording. doc-consistency читает docs/ и openspec/, сверяет документы между собой (факт в двух домах, прямое противоречие, поведение в обзоре вместо спек, ADR без ссылки на design.md и без парного статуса, число без провенанса, заглушка вместо честной строки) и зовётся на шаге синка документации. doc-code-drift читает репозиторий, отвечает на «этот факт ещё верен» и зовётся раз в спринт на сессии. Перечень фактов, сверяемых с кодом, закрыт: имя ветки, команды, пути, зависимости поимённо, настройки с числом, единые точки проекта, capability, проверяемые инварианты. «Сверить архитектуру с кодом» — задача без дна, и агент, которому её поставили, выдаёт правдоподобную труху. Отсюда форма его доклада: начинается таблицей проверенного, а не находками, — по ней видно, чего он не смотрел. Карта домов уехала в устав doc-consistency помеченной копией: устав ссылался на файл плагина, а агент работает в репозитории проекта, где плагина может не быть. copies.py её сторожит. Попутно: докстрока copies.py показывала закрывающие маркеры как <!-- /дом -->, а код требует <!-- /дом: <id> -->. Нашлось первой же попыткой ими воспользоваться. DECISIONS тема 28 (ННОО–ХХЦЦ, следствия 105–108). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,105 @@ RETIRED = {
|
||||
"review": "→ docs/review.md",
|
||||
}
|
||||
|
||||
# --- Слаги в именах файлов --------------------------------------------------
|
||||
|
||||
# Текст документов русский, а **имена файлов английские, kebab-case**. Причина
|
||||
# не в эстетике: имя файла стоит в ссылках из других документов, в коммитах и в
|
||||
# путях, которые люди набирают руками, — а кириллица в пути ломается по-разному
|
||||
# в разных местах и не набирается на английской раскладке.
|
||||
SLUG = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
|
||||
ADR_NAME = re.compile(r"ADR-(\d{4})-(\d{2})-(\d{2})-(.+)")
|
||||
CYRILLIC = re.compile(r"[а-яёА-ЯЁ]")
|
||||
|
||||
# Признаки транслита — и только они. Отличить английское слово от транслита
|
||||
# машина не умеет, поэтому находка идёт **замечанием**: кластеры, которых в
|
||||
# английском практически не бывает, плюс окончания русских падежей.
|
||||
#
|
||||
# Слабые маркеры выброшены намеренно, каждый по своему ложному срабатыванию:
|
||||
# `ost` ловит `post` и `cost`, `sch` — `schema`, `ya` — `yaml`, `nost` —
|
||||
# `nostalgia`, хвост `ii` — `radii`. Набор подобран так, чтобы ложных
|
||||
# срабатываний не было вовсе: правило, краснеющее на правде, приучает
|
||||
# пролистывать весь блок. Цена известна и принята — `sostoyanie-partii`
|
||||
# проходит мимо.
|
||||
#
|
||||
# Тот же приём, что `translit_ish` в tasks.py; скрипты независимы намеренно —
|
||||
# каждый уезжает в чужой проект в одиночку.
|
||||
TRANSLIT_CLUSTER = re.compile(r"zh|kh|shch|tsy|iya|ovanie|enie|stvo")
|
||||
TRANSLIT_TAIL = re.compile(r"(?:ej|oj|ij|yj|yy|aya)$")
|
||||
|
||||
|
||||
def translit_ish(slug: str) -> bool:
|
||||
if TRANSLIT_CLUSTER.search(slug):
|
||||
return True
|
||||
return any(TRANSLIT_TAIL.search(part) for part in slug.split("-"))
|
||||
|
||||
|
||||
def check_slugs(root: Path, rep: Report) -> None:
|
||||
"""Имена файлов канона: латиница kebab-case, у ADR — ещё и форма имени.
|
||||
|
||||
Каталог задач не трогаем: его слаги ведёт и проверяет tasks.py, и вторая
|
||||
проверка того же места разошлась бы с первой.
|
||||
"""
|
||||
docs = root / "docs"
|
||||
if not docs.is_dir():
|
||||
return
|
||||
# Имена, выбранные каноном, а не проектом: их форма задана здесь же.
|
||||
fixed = {"README.md", "template.md"} | ALLOWED_FILES
|
||||
for sub in ("conventions", "research", "adr"):
|
||||
folder = docs / sub
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
for path in sorted(folder.rglob("*.md")):
|
||||
name = path.name
|
||||
rel = path.relative_to(root)
|
||||
if name in fixed:
|
||||
continue
|
||||
stem = path.stem
|
||||
if sub == "adr":
|
||||
m = ADR_NAME.fullmatch(stem)
|
||||
if not m:
|
||||
rep.error(
|
||||
f"{rel}: имя не по форме ADR-ГГГГ-ММ-ДД-slug.md — "
|
||||
f"по имени сортируются записи и ищется дата решения"
|
||||
)
|
||||
continue
|
||||
stem = m.group(4)
|
||||
if CYRILLIC.search(stem):
|
||||
rep.error(
|
||||
f"{rel}: кириллица в имени файла — слаги английские, "
|
||||
f"kebab-case (текст документа при этом русский)"
|
||||
)
|
||||
continue
|
||||
if not SLUG.fullmatch(stem):
|
||||
rep.error(
|
||||
f"{rel}: имя не kebab-case латиницей — только строчные "
|
||||
f"буквы, цифры и одиночные дефисы"
|
||||
)
|
||||
continue
|
||||
if translit_ish(stem):
|
||||
rep.note(
|
||||
f"{rel}: имя похоже на транслит («{stem}») — слаг именуется "
|
||||
f"английским словом по сути, а не записью русского латиницей: "
|
||||
f"транслит нечитаем тому, кто ищет по смыслу. Проверено "
|
||||
f"эвристикой: английское слово от транслита машина не отличает"
|
||||
)
|
||||
check_capability_slugs(root, rep)
|
||||
|
||||
|
||||
def check_capability_slugs(root: Path, rep: Report) -> None:
|
||||
specs = root / "openspec" / "specs"
|
||||
if not specs.is_dir():
|
||||
return
|
||||
for folder in sorted(specs.iterdir()):
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
if CYRILLIC.search(folder.name) or not SLUG.fullmatch(folder.name):
|
||||
rep.error(
|
||||
f"openspec/specs/{folder.name}/: имя capability — латиница "
|
||||
f"kebab-case; оно стоит в ссылках из architecture.md и в спеках"
|
||||
)
|
||||
|
||||
|
||||
DEBT_MARKER = re.compile(r"<!--\s*канон:\s*(.+?)\s*-->")
|
||||
PLACEHOLDER = re.compile(r"<!--\s*заполнить:\s*(.+?)\s*-->")
|
||||
MD_LINK = re.compile(r"\[[^\]]*\]\(\s*<?([^)>\s]+)>?(?:\s+[\"'(][^)]*)?\)")
|
||||
@@ -371,9 +470,10 @@ def report(rep: Report) -> int:
|
||||
print(f" {msg}")
|
||||
|
||||
print(
|
||||
"\nМашина проверила раскладку, ссылки, версию и две сверки с кодом.\n"
|
||||
"Смысловые дубли, оставшееся в architecture.md поведение и достаточность\n"
|
||||
"честной строки в пустом слоте она не проверяет — это суждение агента."
|
||||
"\nМашина проверила раскладку, имена файлов, ссылки, версию и две сверки\n"
|
||||
"с кодом. Согласованность документов между собой и с кодом она не\n"
|
||||
"проверяет — это суждение агентов `doc-consistency` (документ ↔ документ\n"
|
||||
"↔ openspec) и `doc-code-drift` (документ ↔ код)."
|
||||
)
|
||||
if rep.errors:
|
||||
print(f"\nИтог: дрейф, {len(rep.errors)} пунктов.")
|
||||
@@ -394,6 +494,7 @@ def cmd_check(args: argparse.Namespace) -> int:
|
||||
check_version(root, cfg, rep)
|
||||
check_required(root, cfg, rep)
|
||||
check_stray(root, rep)
|
||||
check_slugs(root, rep)
|
||||
check_links(root, rep)
|
||||
check_placeholders_and_debt(root, rep)
|
||||
check_capabilities(root, rep)
|
||||
|
||||
Reference in New Issue
Block a user