- документ самоуправления объявляется ключом governance, а не угадывается по «он один и без ключей оси»: конвенция, потерявшая topic, была от него неотличима и тихо теряла все проверки об отъезде к потребителю - проверка путей канона больше не ловит README.md и READING.md — эти два имени значат что-то и на стороне потребителя - lang.Recognize требует совпадения и слов, и номера версии; директории компонентов сверяются на вложенность, а не только на равенство - у обеих проверок появился --json, а convy sync называет ссылки на темы, которых компонент не взял
217 lines
19 KiB
Markdown
217 lines
19 KiB
Markdown
# 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 нет.
|