# 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 оба манифеста: чтение и запись internal/doc разбор документа: шапка, области правил, блоки internal/suite сборка набора в память, отбор слоёв под компонент internal/project сборка копий в проекте: разделы, маркер, READING.md internal/check проверки: manifest, form, spread, links internal/cli команды, диалог, два режима ``` Зависимость одна — `BurntSushi/toml`, и вторую не заводить. `Undecoded()` этого парсера бесплатно ловит опечатки в ключах манифеста, а инструмент, который копирует файлы и разбирает markdown, не имеет права тянуть за собой дерево чужого кода. Бинарь тоже один, с подкомандой `suite`. Разделять на два дешевле, если задачи разъедутся, но пока они не разъехались. ## Инварианты кода - **В коде нет ни одной темы, ни одного префикса, ни одного пути набора.** Всё это приходит из манифеста. Список проверяемых файлов берётся из `[prefixes.live]`, а не из дерева директорий: это и есть граница «язык употребляет» против «язык цитирует». - **Словарь зашит в бинарь** по паре «версия языка × естественный язык», а не объявляется в манифесте. Когда спецификация языка уедет в отдельный репозиторий и там появятся спеки словарей, источником станут они — подменяется `registry`, тип `Vocabulary` и проверки поверх него не трогаются. - **Что инструмент пишет, инструмент принимает.** Набор, созданный `suite init`, `add` и `rule`, обязан проходить `suite check` без правок. Это проверяет `checkClean` в `internal/cli`; ломать инвариант нельзя. - **Манифест — данные.** Оба манифеста декодируются в структуры и пишутся обратно энкодером целиком. Комментариев в них нет: файл, который машина переписывает, комментарий через круг не проносит, и вид, что проносит, стоит этого комментария в день, когда никто не смотрит. Объяснения — в соседних файлах, которых ни одна команда не касается. - **Непонятый ключ останавливает запись.** Раз запись идёт из структур, ключ, которого в них нет, при сохранении исчез бы. `manifest.save` отказывается, называя ключ: это единственный исход, который его не теряет и не прячет. - **Разбор опирается на разметку, а не на суждение.** Область правила — от заголовка до следующего заголовка любого уровня. Метка открывает блок только первой в абзаце и полужирным. Огороженные блоки кода исключаются везде; инлайн-код вырезается там, где ищутся ссылки, и не вырезается там, где ищутся пути канона. Маркер локальной части — то же самое: `doc.LocalMarker` один на весь инструмент, `doc.Marker()` пропускает огороженные блоки, потому что конвенция о ведении копий этот маркер цитирует. - **Уровень называется ссылкой, а не путём.** Проект ссылается на набор, набор на язык; `source.Ref` разбирает ссылку, `source.Open` отдаёт директорию, которую можно читать. Транспортов два, но `Kind` — перечисление, а не булево: третий (rclone, дерево по https) ожидается, и отказ обязан сначала сказать, чем ссылку сочли, и только потом — что не так. - **Документ самоуправления объявлен, а не угадан.** `governance` в манифесте называет файл, который написан языком конвенций, но не принадлежит теме. Угадывание пробовали — «он один» и «у него нет ключей оси» верны и для набора, у которого единственная конвенция потеряла `topic`, а потеря эта дорогая: файл сохраняет все проверки формы и тихо теряет все проверки об отъезде к потребителю. - **Проверки формы не знают про набор.** `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 в том числе), проверяя вместо этого саму ссылку. Проверка гоняется на каждой правке и в сеть ходить не должна. Пропуск объявляется строкой в выводе: молча не выполненная проверка читается ровно как пройденная. - **Комментариев в манифестах не будет.** Пробовали держать их текстовой правкой — вышло четыре случая порчи данных подряд: комментарий с кавычками становился подпиской, скобка в комментарии обрезала массив. Формат с сохранением комментариев при записи (`go-toml-edit`, YAML через `yaml.Node`) отвергнут как усложнение под задачу, которой нет: манифест машинный. - **`sync` — о наборе файлов, `pull` — о содержимом.** `pull` берёт текст всех подписок заново, и оставленный им дифф и есть смысл запуска. `sync` сверяет манифест и подводит под него раскладку: чего не хватает — собирает, что осиротело — удаляет. Копию с непустой локальной частью не удаляет никогда и завершается с ошибкой, пока она лежит. - **Позиционный аргумент отсекается явно.** `flag` прекращает разбор на первом не-флаге, поэтому лишний аргумент не просто лежит без дела — он прячет все флаги после себя. `noStrayArgs` в командах без позиционных, ручное снятие темы с головы в `convy add`. ## Проверки Семейства повторяют деление из `LANGUAGE.md`, раздел «Что стоит проверять машиной», и это деление держится в коде: `form` — в любом файле, который язык употребляет; `spread` — только в конвенциях, потому что эти проверки о том, что документ уезжает к потребителю. Третья часть списка (взаимоисключительность строк таблицы, покрытие области действия, отвечает ли обоснование на «что сломается») разбором текста не даётся и в коде отсутствует намеренно. Новая проверка заводится вместе с двумя тестами: что она срабатывает и что она **молчит** там, где не должна. Второй важнее: проверка, краснеющая на исправном файле, выключается целиком. Ложные срабатывания собраны в `TestNoFalsePositives` для набора и в `TestCopiesAreSilentOnASoundCopy` для копий. Перед тем как заводить проверку, стоит прогнать её замысел по живому канону (`dev-conventions`): если она покраснеет на исправном наборе, замысел неверен. ## Известные остатки - Набор в поддиректории git-репозитория не адресуется: `#рев` есть, `//путь` нет. Синтаксис, которого не просит ни один набор, не заводится заранее. - Источник у проекта один. Модель нескольких допускает; форма `source = "..."` расширяется до `[sources.имя]`, не ломая существующие манифесты. - `convy add` пишет подписку после сборки. Если сборка прошла, а запись упала, копия останется неучтённой — но это уже поправимо: `convy sync` её либо уберёт, либо соберёт заново, когда тему подпишут. - Ни линтеров, ни CI. Раздача — `go install`, пока этого хватает. ## Тесты ``` 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`, `sync`, `list`, `check`. Обе стороны закончены. Отбор слоёв под компонент — один на обе стороны: `suite.Assemble`. `suite list` показывает, что взял бы компонент, `convy pull` и `convy sync` то же самое пишут в файл; разъехаться они не должны. Линтеров и CI нет.