Files
av 600aba5ee3 закрыты известные остатки
- документ самоуправления объявляется ключом governance, а не угадывается
  по «он один и без ключей оси»: конвенция, потерявшая topic, была от него
  неотличима и тихо теряла все проверки об отъезде к потребителю
- проверка путей канона больше не ловит README.md и READING.md — эти два
  имени значат что-то и на стороне потребителя
- lang.Recognize требует совпадения и слов, и номера версии; директории
  компонентов сверяются на вложенность, а не только на равенство
- у обеих проверок появился --json, а convy sync называет ссылки на темы,
  которых компонент не взял
2026-07-28 10:16:27 +03:00

217 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 нет.