Files
convy/CLAUDE.md
T
av 92bd1f463d манифесты стали данными, заведён convy sync
- убраны комментарии из suite.toml и .conventions.toml: файл, который
  машина переписывает, комментарий через круг не проносит; объяснения
  ушли в README рядом, который suite init теперь заводит
- удалена текстовая правка манифеста целиком — 520 строк ручного
  лексера TOML вместе со всем классом ошибок порчи данных
- запись идёт из структур энкодером; ключ, которого инструмент не
  знает, запись останавливает, а не теряется молча
- convy sync сверяет манифест и подводит под него раскладку файлов:
  чего не хватает — собирает, что осиротело — удаляет, копию с
  локальной частью не трогает никогда
2026-07-28 09:45:10 +03:00

19 KiB
Raw Blame History

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-conventionsREADME.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; ломать инвариант нельзя.
  • Манифест — данные. Оба манифеста декодируются в структуры и пишутся обратно энкодером целиком. Комментариев в них нет: файл, который машина переписывает, комментарий через круг не проносит, и вид, что проносит, стоит этого комментария в день, когда никто не смотрит. Объяснения — в соседних файлах, которых ни одна команда не касается.
  • Непонятый ключ останавливает запись. Раз запись идёт из структур, ключ, которого в них нет, при сохранении исчез бы. 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 до набора не дотягивается и разрешает только ссылки на префиксы самого файла. Открытый вопрос из TOOL.md; закрывается отдельной командой, а не этой.
  • Набор в поддиректории git-репозитория не адресуется: #рев есть, //путь нет. Появится вместе с первым набором, который так лежит.
  • Источник у проекта один. Модель нескольких допускает; форма source = "..." расширяется до [sources.имя] не ломая существующие манифесты.
  • convy add пишет подписку после сборки — если сборка прошла, а запись в манифест упала, копия останется неучтённой. Обратный порядок хуже: подписка без файла отправляет следующий pull искать то, чего не делали.
  • Директории компонентов сверяются на равенство, а не на вложенность. Компонент в docs и компонент в docs/sub манифест пропустит; convy check от двойных находок защищён отдельно (distinct).
  • lang.Recognize при отсутствии словаря с совпавшим номером версии отдаёт первого кандидата, у которого совпали слова. Пока версия в реестре одна, это безвредно; со второй версией того же естественного языка станет неверно.

Тесты

go test ./...                       всё
go test ./internal/check/ -v        проверки, по одному подтесту на случай
gofmt -l . && go vet ./...          перед коммитом

Тесты фикстурные: набор пишется во временную директорию и прогоняется целиком. internal/cli проверяет обе моды, включая диалог — интерактивный режим иначе не покрыть, из шелла он требует терминала. Проектные тесты строят набор теми же командами и подключают его в проект: subscribable в project_test.goretirable плюс 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. Обе стороны закончены по тому, что намечено в TOOL.md; sync в TOOL.md не значится и заведён сверх него.

Отбор слоёв под компонент — один на обе стороны: suite.Assemble. suite list показывает, что взял бы компонент, convy pull и convy sync то же самое пишут в файл; разъехаться они не должны.

Линтеров и CI нет.