# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Язык Общение по репозиторию — на русском. Внутри репозитория язык распределён так, и это распределение проверяется глазами при ревью: - **английский** — комментарии в коде, имена, тексты ошибок, вывод CLI, сообщения и имена тестов; - **русский** — `README.md`, `CLAUDE.md`, сообщения коммитов; - **язык проверяемого набора** — содержимое тестовых фикстур и литералы словарей в `internal/lang`. Это данные под проверкой, а не текст инструмента, и переводить их нельзя: сломается то, что они проверяют. Слова словаря законно попадают внутрь английских сообщений, потому что подставляются из набора: `rule GTIM-1 has no ПОЧЕМУ block`. Инструмент говорит по-английски и цитирует то, чем записан проверяемый набор. ## Что это CLI для управления конвенциями. Модель здесь не описывается: она живёт в репозитории набора `dev-conventions` — `README.md` (набор и копии), `LANGUAGE.md` (форма правила и список проверок), `GUIDE.md` (ведение набора, префикс META), `TOOL.md` (решения об инструменте). При расхождении истина там, а не в коде и не здесь. Пользовательская сторона — в `README.md`. Ниже — то, что нужно знать, правя код. ## Раскладка ``` internal/lang словарь: реестр «версия языка × естественный язык» internal/source ссылки между уровнями: путь на диске, git-репозиторий internal/manifest suite.toml и .conventions.toml — чтение и текстовая правка internal/doc разбор документа: шапка, области правил, блоки internal/suite сборка набора в память, отбор слоёв под компонент internal/project сборка копий в проекте: разделы, маркер, READING.md internal/check проверки: manifest, form, spread, links internal/cli команды, диалог, два режима ``` Зависимость одна — `BurntSushi/toml`. Вторую не заводить: так решено в `TOOL.md`, и `Undecoded()` этого парсера бесплатно ловит опечатки в ключах манифеста. ## Инварианты кода - **В коде нет ни одной темы, ни одного префикса, ни одного пути набора.** Всё это приходит из манифеста. Список проверяемых файлов берётся из `[prefixes.live]`, а не из дерева директорий: это и есть граница «язык употребляет» против «язык цитирует». - **Словарь зашит в бинарь** по паре «версия языка × естественный язык», а не объявляется в манифесте. Когда спецификация языка уедет в отдельный репозиторий и там появятся спеки словарей, источником станут они — подменяется `registry`, тип `Vocabulary` и проверки поверх него не трогаются. - **Что инструмент пишет, инструмент принимает.** Набор, созданный `suite init`, `add` и `rule`, обязан проходить `suite check` без правок. Это проверяет `checkClean` в `internal/cli`; ломать инвариант нельзя. - **Манифест правится текстом, а не энкодером.** В `suite.toml` комментариев больше, чем данных. `manifest.AddEntry` вставляет запись, подстраиваясь под порядок таблицы: отсортированную по алфавиту держит отсортированной, упорядоченную вручную дополняет в конец. Комментарий, отделённый пустой строкой, принадлежит таблице **ниже** себя. - **Разбор опирается на разметку, а не на суждение.** Область правила — от заголовка до следующего заголовка любого уровня. Метка открывает блок только первой в абзаце и полужирным. Огороженные блоки кода исключаются везде; инлайн-код вырезается там, где ищутся ссылки, и не вырезается там, где ищутся пути канона. - **Уровень называется ссылкой, а не путём.** Проект ссылается на набор, набор на язык; `source.Ref` разбирает ссылку, `source.Open` отдаёт директорию, которую можно читать. Транспортов два, но `Kind` — перечисление, а не булево: третий (rclone, дерево по https) ожидается, и отказ обязан сначала сказать, чем ссылку сочли, и только потом — что не так. - **Проверки формы не знают про набор.** `checkRules`, `checkVersionLine`, `checkModalsOutside` и прочие принимают `lang.Vocabulary`, а не `*suite.Suite` — иначе `convy check` в проекте пришлось бы писать заново. Копия несёт язык строкой о версии, и `lang.Recognize` читает его оттуда: манифеста рядом нет. ## Решения, которые уже приняты Их не пересматривают без явной просьбы — каждое обсуждалось и стоило времени. - **Порядок правил в файле — по читаемости, а не по номерам.** Номер стабилен и не переиспользуется, поэтому «порядок по номерам» означал бы «порядок по времени написания» навсегда. Проверяется сплошность нумерации, а не возрастание. - **Ссылка на чужую тему вне нормы разрешена** (META-20): обоснование, потерявшее адресата, деградирует честно. Внутри своей темы ссылаться можно только в базовый слой, и это проверяется во всём документе, а не только в норме: гарантированно присутствует в копии один базовый слой. - **Заголовок правила — объявление, а не ссылка.** При разборе ссылок строки заголовков пропускаются. - **Лок-файла, `push`, отчёта о расхождении и перенумерации не будет.** Модель отвергает каждое явно. - **Кэша источника нет.** Клон делается заново и удаляется вместе с `Tree`. Кэш экономит второй клон и возвращает вопрос, что в нём протухло; на «что было в прошлый раз» отвечает git потребителя. Если станет дорого, кэш прячется за `source.Tree` и наружу не виден. - **`file://` — это git, а не директория.** Простой путь уже означает «эта директория, как она лежит», вместе с грязным рабочим деревом; `file://` означает «тот же репозиторий в закоммиченном виде». Ради этой разницы оба написания и существуют — и ради неё же git-транспорт тестируется без сети. - **Слой ниже первого становится разделом.** Заголовки опускаются на уровень, строка о версии языка выбрасывается у всех, кроме первого. Расширение реализует и сужает базу, а не стоит рядом с ней, — поэтому правила базы в копии на `###`, а правила языкового слоя на `####`. Проверка копий уровень заголовка правила не требует, лестницу заголовков — требует. - **`convy check` до набора не дотягивается.** Форма правила одна и та же, локальные правила на `X` записаны по ней же, а проверять своё нужно без сети и без знания, откуда копии приехали. - **Целостность набора проверяется локально.** Когда `[language] source` заполнен, `suite check` не тянет описание языка и пропускает проверки документов о языке (META-30 в том числе), проверяя вместо этого саму ссылку. Проверка гоняется на каждой правке и в сеть ходить не должна. ## Проверки Семейства повторяют деление из `LANGUAGE.md`, раздел «Что стоит проверять машиной», и это деление держится в коде: `form` — в любом файле, который язык употребляет; `spread` — только в конвенциях, потому что эти проверки о том, что документ уезжает к потребителю. Третья часть списка (взаимоисключительность строк таблицы, покрытие области действия, отвечает ли обоснование на «что сломается») разбором текста не даётся и в коде отсутствует намеренно. Новая проверка заводится вместе с двумя тестами: что она срабатывает и что она **молчит** там, где не должна. Второй важнее: проверка, краснеющая на исправном файле, выключается целиком. Ложные срабатывания собраны в `TestNoFalsePositives`. Перед тем как заводить проверку, стоит прогнать её замысел по живому канону (`dev-conventions`): если она покраснеет на исправном наборе, замысел неверен. ## Известные остатки - Набор без документа самоуправления, в котором конвенция потеряла `topic`, проскочит: признаков «этот документ один» и «у него нет ключей слоя» не хватает. Закрывается маркером в манифесте — правка формата, не сделана. - Проверка пути канона считает путём любой токен `*.md`, который резолвится в файл набора; упоминание `README.md` в конвенции она пометит ошибочно. На текущем каноне не срабатывает. - Машиночитаемого вывода находок (`--json`) нет. - Предупреждения о висячей ссылке на неподписанную тему нет: `convy check` до набора не дотягивается и разрешает только ссылки на префиксы самого файла. Открытый вопрос из `TOOL.md`; закрывается отдельной командой, а не этой. - Набор в поддиректории git-репозитория не адресуется: `#рев` есть, `//путь` нет. Появится вместе с первым набором, который так лежит. - Источник у проекта один. Модель нескольких допускает; форма `source = "..."` расширяется до `[sources.имя]` не ломая существующие манифесты. - `convy add` пишет подписку после сборки — если сборка прошла, а запись в манифест упала, копия останется неучтённой. Обратный порядок хуже: подписка без файла отправляет следующий `pull` искать то, чего не делали. ## Тесты ``` go test ./... всё go test ./internal/check/ -v проверки, по одному подтесту на случай gofmt -l . && go vet ./... перед коммитом ``` Тесты фикстурные: набор пишется во временную директорию и прогоняется целиком. `internal/cli` проверяет обе моды, включая диалог — интерактивный режим иначе не покрыть, из шелла он требует терминала. Проектные тесты строят набор теми же командами и подключают его в проект: `subscribable` в `project_test.go` — `retirable` плюс `READING.md`, без которого копиям нечего везти рядом. Git-транспорт проверяется на локальном репозитории через `file://` и пропускается, если `git` не найден. Сети тесты не требуют. ## Коммиты Русский, строчная буква, без точки в конце, прошедшее время или страдательный залог. Изредка область через двоеточие (`suite check:`). Тело — маркированный список на 2–4 пункта с переносом по ~76 колонок, объясняет почему. Conventional Commits и `Co-Authored-By` не используются. ## Состояние Наборная сторона: `init`, `add`, `rule`, `retire`, `list`, `check`. Проектная: `init`, `add`, `pull`, `list`, `check`. Обе стороны закончены по тому, что намечено в `TOOL.md`. Отбор слоёв под компонент — один на обе стороны: `suite.Assemble`. `suite list` показывает, что взял бы компонент, `convy pull` то же самое пишет в файл; разъехаться они не должны. Линтеров и CI нет.