- TOOL.md в каноне удалён как выполнивший задачу стартовой точки; всё, что из него осталось живым, переехало сюда - правило одной зависимости и решение про один бинарь стоят теперь сами по себе, а не со ссылкой на удалённый файл - в известные остатки добавлены два открытых вопроса оттуда: способ раздачи бинарей и опечатка convey в текстах
20 KiB
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) ожидается, и отказ обязан сначала сказать, чем ссылку сочли, и только потом — что не так. - Проверки формы не знают про набор.
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): если она покраснеет на исправном наборе, замысел неверен.
Известные остатки
- Набор без документа самоуправления, в котором конвенция потеряла
topic, проскочит: признаков «этот документ один» и «у него нет ключей слоя» не хватает. Закрывается маркером в манифесте — правка формата, не сделана. - Проверка пути канона считает путём любой токен
*.md, который резолвится в файл набора; упоминаниеREADME.mdв конвенции она пометит ошибочно. На текущем каноне не срабатывает. - Машиночитаемого вывода находок (
--json) нет. - Предупреждения о висячей ссылке на неподписанную тему нет:
convy checkдо набора не дотягивается и разрешает только ссылки на префиксы самого файла. Закрывается отдельной командой, а не этой: ей нужен список подписок. - Набор в поддиректории git-репозитория не адресуется:
#ревесть,//путьнет. Появится вместе с первым набором, который так лежит. - Источник у проекта один. Модель нескольких допускает; форма
source = "..."расширяется до[sources.имя]не ломая существующие манифесты. convy addпишет подписку после сборки — если сборка прошла, а запись в манифест упала, копия останется неучтённой. Обратный порядок хуже: подписка без файла отправляет следующийpullискать то, чего не делали.- Директории компонентов сверяются на равенство, а не на вложенность. Компонент
в
docsи компонент вdocs/subманифест пропустит;convy checkот двойных находок защищён отдельно (distinct). lang.Recognizeпри отсутствии словаря с совпавшим номером версии отдаёт первого кандидата, у которого совпали слова. Пока версия в реестре одна, это безвредно; со второй версией того же естественного языка станет неверно.- Опечатка
conveyвместоconvyв текстах ничем не ловится. Слова отличаются одной буквой, и оба выглядят словами. - Способ раздачи готовых бинарей не выбран.
egetумеет релизы GitHub, а git здесь свой (git.vakhrushev.me) — тянет ли он релизы Gitea, не проверено; иначе остаются прямой URL или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 нет.