From 9c86d9f2dea179f275b059eeffa6ec62e45b17ee Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sun, 26 Jul 2026 22:01:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B=20=D0=BA=D0=B0=D0=BA=20=D0=B0=D0=B4=D1=80?= =?UTF-8?q?=D0=B5=D1=81=D0=B0=D1=82=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=BB=D0=BE=D1=81=D0=BA=D0=B8=D0=B9=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - компонент — область репозитория, где выбранные слои действуют одновременно; сборка идёт по разу на компонент, у каждого своя директория копий, подписка и локальная часть, секции [components.<имя>] в манифесте - плоский набор описан как низкий конец модели, а не отдельный режим: тема с одним слоем собирается копированием, ключи оси и lang/stack не пишутся - в TODO заведён вопрос о реестре значений осей и судьбе extends: --- CLAUDE.md | 17 ++++++ README.md | 154 +++++++++++++++++++++++++++++++++++++++++++---------- TODO.md | 34 +++++++++--- TOOL.md | 39 ++++++++++---- suite.toml | 10 ++-- 5 files changed, 206 insertions(+), 48 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 96fae41..fcbad95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -143,6 +143,8 @@ code in this repository. - META-33: правило стоит в теме, чей вопрос оно решает. Тема — решение, а не вещество: «время» проходит через несколько решений сразу, и правило о колонках БД принадлежит схеме, а не времени. +- META-37: имя темы называет решение и адресата, а не роль части проекта: + `logging` и `client-logging`, но не `logging-backend`/`logging-frontend`. - META-34: тема нужна потребителю целиком — подписка берёт её без остатка. Если два правдоподобных потребителя хотят непересекающиеся части, между ними и проходит граница. @@ -160,6 +162,21 @@ code in this repository. потребителей. `extends: arch/<файл>.md` в шапке — документация связи, а не механизм; слой только реализует и сужает базу, но не отменяет её (META-35). +META-38: ось объявлена в шапке ключами `lang:` и `stack:`, а не выведена из +пути; без обоих ключей файл — базовый слой темы. Директория повторяет +объявленное для человека. Осей может не быть вовсе: набор, где у темы один +слой, — низкий конец той же модели, а не особый режим. + +## Компоненты + +Компонент — область репозитория, где все выбранные слои действуют +одновременно (`sqlite` и `postgres` — да, go и javascript — никогда). Уровней +три: набор → проект → компонент. Сборка прогоняется по разу на компонент, у +каждого своя директория копий, своя подписка и своя локальная часть; в +`.conventions.toml` они записаны секциями `[components.<имя>]` с ключами +`dir`, `lang`, `stack`, `topics`. Компонент пишется всегда, даже когда он +один. Директории компонентов различны — этим копии и разводятся. + ## Оформление файла Шапка `topic:` и `prefix:` (плюс `extends:`) → `# Тема` → вводная проза → diff --git a/README.md b/README.md index 4fe5df2..7e129cf 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,27 @@ conventions/ stack/<стек>/ привязка к инструменту, хранилищу, транспорту ``` -Оси — раскладка **канона**; в репозитории копия лежит плоско, файлом на -тему. Пути файлов даются относительно `conventions/` -(`arch/db-identifiers.md`) и адресуют исходник канона, а не место в копии. -На **правила** ссылаются идентификатором без пути: `KEYS-5`. Префикс -уникален по всему канону (он перечислен в манифесте набора), поэтому -идентификатор не зависит ни от оси, ни от того, как собран файл у -потребителя. +Ось файл **объявляет в шапке**, а не наследует от директории (META-38): + +```yaml +topic: logging +prefix: SLOG +lang: go +``` + +Ключей оси нет — базовый слой темы. Слова те же, что в подписке потребителя +(`lang`, `stack`), так что переводить между двумя сторонами нечего. Дерево +директорий повторяет объявленное для человека и остаётся раскладкой +**канона**: в репозитории копия лежит плоско, файлом на тему. Пути файлов +даются относительно `conventions/` (`arch/db-identifiers.md`) и адресуют +исходник канона, а не место в копии. На **правила** ссылаются идентификатором +без пути: `KEYS-5`. Префикс уникален по всему канону (он перечислен в +манифесте набора), поэтому идентификатор не зависит ни от оси, ни от того, как +собран файл у потребителя. + +Объявление вдобавок выражает то, чего дерево не умеет: слой, осмысленный +только при совпадении языка и инструмента сразу (`lang: go` и `stack: slog` в +одной шапке). Тест — по тому, замена чего убивает правило: @@ -84,6 +98,31 @@ conventions/ пласт `stack/sqlite/` (типы колонок). Это не принцип, а незавершённая работа. +## Плоский набор + +Осей может не быть вовсе. Набор, где у каждой темы ровно один слой, — не +особый режим, а низкий конец той же модели: сборка «база → язык → стек» +на нём даёт просто копию файла. + +``` +conventions/ + logging.md topic: logging, prefix: LOGS + errors.md topic: errors, prefix: ERRS + time.md topic: time, prefix: TIME +``` + +Ключей оси в шапках нет, `lang` и `stack` в подписке не пишутся — выбирать +не из чего. Так дешевле начинать и так выглядит чужой набор, которому три оси +объяснять незачем, чтобы записать пять правил. + +Цена платится при росте, и она не в инструменте: когда плоская тема +расслаивается, уехавшие в новый файл правила получают новый префикс и новую +нумерацию. Смягчается это тем, что уехавшее правило остаётся на месте +заглушкой СНЯТО с новым адресом в причине (META-31, META-32), и тем, что +резать нужно правильной стороной: база остаётся в исходном файле со своими +идентификаторами, а наружу уезжает специфичное. Если второй язык виден +заранее, дешевле сразу разложить по осям. + ## Темы **Тема — набор правил об одном фокусе разработки:** время, конфигурация, @@ -109,6 +148,13 @@ prefix: KEYS репозиториях — в шапке `origin:` каждой копии, в подписке манифеста, в тексте ссылок, — и выданное второй теме начинает указывать на другой набор правил. +Раз имя вечно, называют тему **решением и его адресатом**, а не ролью части +конкретного проекта (META-37). Логи сервера и логи браузера — это `logging` и +`client-logging`, а не `logging-backend` и `logging-frontend`: роль +принадлежит сегодняшнему устройству одного репозитория и молча начинает врать, +а суффиксная пара вдобавок навязывает чтение «две разновидности одного», хотя +по границе темы это разные решения — общего у них три правила из сорока. + ## Префиксы Каждый файл канона объявляет в шапке свой префикс правил: @@ -143,24 +189,60 @@ extends: arch/db-identifiers.md репозиторий на базу просто не подписан. `extends` — документация связи, а не механизм: за тем, чтобы база лежала -рядом, никто не следит. +рядом, никто не следит. С объявленной осью база к тому же находится сама — +это слой той же темы без ключей `lang` и `stack`, — так что ключ остаётся +подсказкой человеку и ничего не выбирает. + +## Компонент — адресат сборки + +Подписка принадлежит репозиторию, а собранный документ адресован не +репозиторию, а куску кода. Пока проект однороден, разницы нет; два языка её +проявляют: `lang = ["go", "javascript"]` склеили бы в один файл го-слой и +js-слой, из которых к правимому коду относится ровно половина. + +**Компонент — область репозитория, где все выбранные слои действуют +одновременно.** `sqlite` и `postgres` в теме схемы действуют вместе — разные +таблицы одного сервиса; го-слой и js-слой не действуют вместе никогда, потому +что строка кода написана на чём-то одном. Компонент поэтому совпадает с тем, +у чего один язык, один набор инструментов и один вид приложения (META-36). + +Уровней в модели становится три: набор → проект → компонент. Сборка не +меняется — та же линейка «база → язык → стек», прогнанная по разу на +компонент. ## Копия в репозитории Копия плоская: **один файл на тему**, слои осей идут внутри него секциями в -порядке `arch` → язык → стек. Пути канона в копии не воспроизводятся. +порядке база → язык → стек. Пути канона в копии не воспроизводятся. Каждый +компонент получает свою директорию: ``` -docs/conventions/ +.conventions.toml +backend/docs/conventions/ README.md собственный, не собирается READING.md как читать конвенцию — приезжает из канона + logging.md база + lang/go + stack/slog time.md arch/time.md + lang/go/time.md - db-identifiers.md arch/db-identifiers.md + lang/go/db-identifiers.md - app-directories.md arch/… + stack/ansible/… +web/docs/conventions/ + READING.md + client-logging.md база + lang/javascript + stack/express ``` Так конвенция остаётся самодостаточным документом: тот, кто проверяет по ней код — человек или агент, — читает один файл и не собирает тему из трёх мест. +При одном компоненте это ровно прежняя раскладка — `docs/conventions/` в +корне. + +Директории компонентов различны, и это единственное, что разводит копии: +`logging.md` двух компонентов — разные файлы с одинаковым `origin: logging`, +и какой из них какой, сборщик знает по манифесту, а читатель — по пути. +Локальные части у них независимы, ради чего всё и затевается: правило, +механизированное линтером в go-компоненте, в js-компоненте не механизировано, +и один общий файл этого не записал бы. + +`READING.md` лежит рядом с копиями, то есть по одному на компонент. Файл +генерируемый, а директория с правилами обязана объяснять себя тому, кто в неё +попал. Имена в директории делятся на три вида: `README.md` принадлежит репозиторию и сборщик его не трогает, `READING.md` принадлежит канону и перезаписывается @@ -230,7 +312,7 @@ MIGR-6 не соблюдается в `show_history`, `queue`: составны | Файл | Где лежит | Что описывает | |---|---|---| | `suite.toml` | в наборе | сам набор: язык, темы, префиксы правил | -| `.conventions.toml` | в проекте | подключение: откуда копии, какие темы, язык, стек | +| `.conventions.toml` | в проекте | подключение: откуда копии, компоненты и их подписки | Манифест набора — единственное место, где перечислены оба идентификатора канона; правила у них общие, поэтому и файл один. Манифест подключения @@ -239,17 +321,29 @@ MIGR-6 не соблюдается в `show_history`, `queue`: составны ```toml source = "ssh://git@git.vakhrushev.me:2222/av/dev-conventions.git" +[components.backend] +dir = "backend/docs/conventions" lang = ["go"] -stack = ["sqlite", "htmx"] +stack = ["slog", "sqlite"] +topics = ["logging", "errors", "time"] -topics = ["time", "config", "db-identifiers"] +[components.web] +dir = "web/docs/conventions" +lang = ["javascript"] +stack = ["express"] +topics = ["client-logging"] ``` `lang` и `stack` выбирают строку разреженной матрицы: файл темы собирает -только те слои, которые репозиторию подходят. `topics` — подписка, именами из -манифеста набора; списка подписчиков у канона по-прежнему нет, список подписок -есть -только у потребителя. +только те слои, которые компоненту подходят, и совпадают со словами, которыми +слой объявил свою ось. `topics` — подписка, именами из манифеста набора; +списка подписчиков у канона по-прежнему нет, список подписок есть только у +потребителя. + +Компонент пишется всегда, даже когда он один: сокращённая плоская форма +сэкономила бы три строки и завела бы второй способ сказать то же самое. +Имя компонента при этом не служебное — им сборщик отвечает, что и куда +собрал. У плоского набора `lang` и `stack` в компоненте просто отсутствуют. Как именно инструмент добирается до канона — путь на диске, git, HTTP — дело инструмента, а не модели. Манифест отвечает на два вопроса: откуда это @@ -265,20 +359,25 @@ topics = ["time", "config", "db-identifiers"] любого другого документа. Это главный канал тихого дрейфа, поэтому `AGENTS.md` каждого потребителя должен явно говорить: -> Файлы в `docs/conventions/` с шапкой `origin:` — копии из канона -> `dev-conventions`. Репозиторное пишется только ниже ``; -> всё выше маркера перезаписывается при обновлении. Своё правило — с -> префиксом на `X`. +> Файлы с шапкой `origin:` в директориях конвенций (пути — в +> `.conventions.toml`) — копии из канона `dev-conventions`. Репозиторное +> пишется только ниже ``; всё выше маркера +> перезаписывается при обновлении. Своё правило — с префиксом на `X`. ## Команды ```bash -conv list # какие темы есть в каноне +conv list # какие темы есть в каноне и что подключено conv add time # добавить тему в манифест и собрать файл +conv add time --for backend # то же, когда компонентов несколько conv pull # пересобрать всё, что перечислено в манифесте # (и обновить READING.md рядом с копиями) +conv pull --for web # только один компонент ``` +При одном компоненте `--for` не нужен. При нескольких команда без него не +угадывает, а отказывает и перечисляет имена. + Отчёт о том, что изменилось, отдельной командой не выдаётся: после `pull` его показывает `git diff`, а решение — принять, поправить или откатить — принимает человек перед коммитом. @@ -315,6 +414,7 @@ conv pull # пересобрать всё, что переч прежнюю: зеркальное дерево копий вместо плоского, именованные регионы `` вместо одного маркера, `origin_hash` в шапке и команды `status`, `diff`, `push`; `READING.md` рядом с копиями он тоже пока не -кладёт. Сами конвенции уже приведены к новой модели — именованных регионов в -каноне нет. Ни один репозиторий-потребитель не -подключён, поэтому переход никого не ломает. +кладёт, компонентов и объявленной оси не знает и выбирает слои по пути. Сами +конвенции уже приведены к новой модели — именованных регионов в каноне нет, +ось объявлена в шапках. Ни один репозиторий-потребитель не подключён, поэтому +переход никого не ломает. diff --git a/TODO.md b/TODO.md index 340364c..7f1b974 100644 --- a/TODO.md +++ b/TODO.md @@ -69,7 +69,25 @@ API, а норму при этом нельзя поправить, не зад # Канон и подключение -## 4. Пары слоёв и темы без базы +## 4. Значения осей нигде не зарегистрированы + +Ось теперь объявлена в шапке (META-38), но её значения не сверяются ни с чем: +`lang: golang` вместо `lang: go` соберётся молча — слой просто не попадёт ни в +одну копию. Это ровно та болезнь, от которой лечили тему (META-28): объявление +без реестра проверяется только глазами. + +Напрашивается секция в `suite.toml` рядом с `[topics.live]` и +`[prefixes.live]` — перечень живых языков и стеков с однострочным описанием, и +те же правила выбытия. Против: третий реестр в манифесте, а значений сегодня +три (`go`, `ansible`, `htmx`). За: словарь общий у двух сторон — им же +потребитель пишет `lang` и `stack` в своём компоненте, и опечатка там стоит +столько же. + +Заодно решается судьба `extends:`: с объявленной осью база находится сама — +это слой той же темы без ключей оси, — так что ключ остался подсказкой +человеку и кандидат на снятие. + +## 5. Пары слоёв и темы без базы Отложено сознательно, но список стоит держать перед глазами: @@ -90,9 +108,9 @@ API, а норму при этом нельзя поправить, не зад - Вынос арх-ядра из `errors` и `logging` закроет две хрупкие ссылки из `web-ui` (стек) в go-слой — единственные ссылки стек → язык в каноне. -## 5. Восемь тем не прогнаны по границе +## 6. Восемь тем не прогнаны по границе -Критерии границы записаны правилами (META-33 … META-36), но ни одна тема по +Критерии границы записаны правилами (META-33 … META-37), но ни одна тема по ним не прочитана. Работа по одной теме: выписать вопрос темы одной фразой и пройти по правилам, помечая чужие. @@ -103,24 +121,24 @@ API, а норму при этом нельзя поправить, не зад (20 символов в БД), GTIM-8 и GTIM-9 (UTC и точность в логах) выносят вердикты тем `db-schema` и `logging`. Остаток — представление момента, единая точка «сейчас», длительность и часы, не-UTC на отображении — тема настоящая. Разрез -попутно снимает `extends: arch/time.md` из вопроса 4. +попутно снимает `extends: arch/time.md` из вопроса 5. `logging` лежит целиком в `lang/go`, хотя внутри три страта: уровень по адресату, «ошибка логируется один раз на границе», секреты — не про Go; `JSONHandler`, `stdout`, разбор через `jq`/DuckDB — про стек; SLOG-30…33 (входящий запрос, healthcheck, 4xx) — про вид приложения (META-36), то есть -про веб-сервис. Здесь же лежит невыделенное арх-ядро из вопроса 4. +про веб-сервис. Здесь же лежит невыделенное арх-ядро из вопроса 5. Отдельно проверить обратное: `errors` без арх-слоя — не дефект. Обработка отказа в плейбуке (`failed_when`, `block`/`rescue`, идемпотентность) го-шные правила не сужает, а решает другую задачу, значит для плейбуков это своя тема, а не слой в `errors` (META-35). -## 6. Подключение к репозиториям +## 7. Подключение к репозиториям Ничего ещё не подключено. Кандидаты — jellybit и pet-project-server. Понадобится: заполнить локальную часть копий тем, что сейчас в этих репозиториях записано по факту; обёртка в раннере (`inv conventions` / `task conventions`, единый интерфейс команд у трёх ansible-репозиториев); -строка в `AGENTS.md` каждого потребителя про то, что файлы в -`docs/conventions/` — копии. +строка в `AGENTS.md` каждого потребителя про то, что файлы в директориях +конвенций — копии. Компонент у обоих кандидатов один, но записывается явно. diff --git a/TOOL.md b/TOOL.md index 97023f7..8c0c44c 100644 --- a/TOOL.md +++ b/TOOL.md @@ -28,12 +28,19 @@ | Уровень | Английский | Русский | Что там лежит | |---|---|---|---| | набор | `suite` | набор | `conventions/`, манифест набора, обвязка | -| проект | `project` | проект | `docs/conventions/`, манифест подключения, копии | +| проект | `project` | проект | манифест подключения, компоненты | +| компонент | `component` | компонент | директория копий: один язык, один стек, один вид приложения | `package`, `bundle`, `library` не берём: они тащат багаж менеджеров зависимостей — версии, разрешение, лок, — которого в модели нет. `set` не годится в CLI: в позиции подкоманды читается глаголом. +Компонент — адресат сборки: подписка принадлежит проекту, а собранный файл +читает тот, кто правит конкретный код. Определение — область, где все +выбранные слои действуют одновременно (`sqlite` и `postgres` — да, go и +javascript — никогда). Уровнем CLI компонент не становится: это аргумент +`--for`, а не подкоманда. + «Канон» — имя этого конкретного набора, а не термин уровня; в общих формулировках употребляется «набор». «Потребитель» — слово про роль репозитория, а не про уровень. @@ -55,10 +62,15 @@ convy pull пересобрать подписанное convy list что подключено и что можно взять convy check проверить форму того, что здесь -convy suite check целостность набора: префиксы, темы, ссылки, форма +convy suite check целостность набора: префиксы, темы, оси, ссылки, форма convy suite new новая тема: шапка, префикс, запись в манифест ``` +Проектные команды принимают `--for <компонент>`. При одном компоненте флаг не +нужен; при нескольких команда без него отказывает и перечисляет имена — тот +же принцип, что и с контекстом: наугад не делается ничего. `convy list` +группирует вывод по компонентам. + Граница проходит не по «проектное против наборного», а по «частое и повсеместное» против «только у автора». Поэтому `check` остаётся наверху: форма правила одна и та же, локальные правила проекта на `X`-префиксах @@ -84,24 +96,33 @@ convy suite new новая тема: шапка, префикс, запис Они расходятся по частоте, по адресату и по тому, что считается провалом. **Целостность набора.** Префиксы уникальны и не переиспользованы, шапка -совпадает с манифестом, тема объявлена и зарегистрирована, у каждого правила +совпадает с манифестом, тема объявлена и зарегистрирована, ось объявлена +ключами и у темы не больше одного базового слоя, у каждого правила модальность с нормой и обоснование либо заглушка, нумерация сплошная, ссылки разрешаются, путей набора в тексте конвенции нет, строка о версии языка на месте. Запускается в наборе при каждой правке; провал — ошибка. -**Установка в проект.** Манифест подключения, сборка файла темы из слоёв, -сохранение локальной части, `READING.md` рядом с копиями. Запускается в -проекте изредка; провал чаще означает «посмотри глазами», чем «ошибка». -Отчёта «набор ушёл вперёд» нет: его делает `git diff` после пересборки. +**Установка в проект.** Манифест подключения, сборка файла темы из слоёв на +каждый компонент, сохранение локальной части, `READING.md` рядом с копиями. +Запускается в проекте изредка; провал чаще означает «посмотри глазами», чем +«ошибка». Отчёта «набор ушёл вперёд» нет: его делает `git diff` после +пересборки. ## Что делает установка Подробности — в README, раздел «Копия в репозитории». Коротко, что важно для реализации: +- сборка идёт **по разу на компонент**, в директорию `dir` из его секции; + директории компонентов обязаны различаться — иначе копии столкнутся + именами, и это ошибка манифеста, а не повод переименовывать файлы; - копия плоская, **один файл на тему**; слои идут секциями в порядке - `arch` → язык → стек, выбор слоёв — по `lang` и `stack` из манифеста - подключения; + база → язык → стек, выбор слоёв — по `lang` и `stack` компонента, сверяемым + с ключами оси в шапке слоя (META-38), а не с путём файла в наборе; слой без + ключей оси — базовый и попадает в копию всегда; +- набор может быть плоским: у темы один слой, ключей оси нет, `lang` и + `stack` в компоненте отсутствуют. Отдельной ветки в коде это не требует — + сборка из одного слоя есть копирование; - шапка копии — только `origin:` с именем темы; отпечатков и дат нет; - всё ниже маркера `` переживает пересборку, всё выше перезаписывается; маркер ставит сборщик; diff --git a/suite.toml b/suite.toml index bb77551..7a6e8e9 100644 --- a/suite.toml +++ b/suite.toml @@ -32,10 +32,12 @@ reading = "READING.md" # БД. Имя записывается латиницей; рекомендуется нижний kebab-case, но годится # любой идентификатор, пригодный для имени файла. # -# Тема — единица подписки и единица сборки: потребитель перечисляет темы в -# своём манифесте, а сборщик складывает в один файл все слои темы в порядке -# arch → язык → стек. Слои узнают друг друга по объявленному имени, а не по -# имени файла: файл конвенции несёт тему в шапке (`topic: time`). +# Тема — единица подписки; собирается она на каждый компонент проекта, все +# слои темы в один файл в порядке база → язык → стек. Слои узнают друг друга +# по объявленному имени, а не по имени файла: файл конвенции несёт тему в +# шапке (`topic: time`), а свою ось — ключами `lang:` и `stack:` там же +# (META-38). Слой без ключей оси — базовый; тем, у которых слой один, +# директории осей не нужны вовсе. # # Имя темы не переименовывается и не переиспользуется: на тему ссылаются # словом — из текста конвенций («конвенция `logging`»), из подписки в